Spring Data RESTでリレーションシップを扱う

Spring Data RESTでのリレーションシップの使用

1. 概要

この記事では、how to work with relationships between entities in Spring Data RESTについて見ていきます。

定義可能な各タイプの関係を考慮して、Spring Data RESTがリポジトリに対して公開する関連付けリソースに焦点を当てます。

余分な設定を避けるために、例ではH2埋め込みデータベースを使用します。 必要な依存関係のリストは、Introduction to Spring Data RESTの記事で確認できます。

 

そして、最初のget started with Spring Data RESTを探している場合は、次の方法で実行を開始できます。

[.iframe-fluid] ##

2. 一対一の関係

2.1. データモデル

@OneToOneアノテーションを使用して、1対1の関係を持つ2つのエンティティクラスLibraryAddressを定義しましょう。 アソシエーションは、アソシエーションのLibrary側によって所有されます。

@Entity
public class Library {

    @Id
    @GeneratedValue
    private long id;

    @Column
    private String name;

    @OneToOne
    @JoinColumn(name = "address_id")
    @RestResource(path = "libraryAddress", rel="address")
    private Address address;

    // standard constructor, getters, setters
}
@Entity
public class Address {

    @Id
    @GeneratedValue
    private long id;

    @Column(nullable = false)
    private String location;

    @OneToOne(mappedBy = "address")
    private Library library;

    // standard constructor, getters, setters
}

@RestResourceアノテーションはオプションであり、エンドポイントをカスタマイズするために使用できます。

We must be careful to have different names for each association resource。 それ以外の場合は、JsonMappingExceptionというメッセージが表示されます:“Detected multiple association links with same relation type! Disambiguate association”

アソシエーション名はデフォルトでプロパティ名になり、@RestResourceアノテーションのrel属性を使用してカスタマイズできます。

@OneToOne
@JoinColumn(name = "secondary_address_id")
@RestResource(path = "libraryAddress", rel="address")
private Address secondaryAddress;

上記のsecondaryAddressプロパティをLibraryクラスに追加すると、addressという名前の2つのリソースがあり、競合が発生します。

これを解決するには、rel属性に別の値を指定するか、RestResourceアノテーションを省略して、リソース名がデフォルトでsecondaryAddressになるようにします。

2.2. リポジトリ

expose these entities as resourcesにするために、CrudRepositoryインターフェースを拡張して、それぞれに2つのリポジトリインターフェースを作成しましょう。

public interface LibraryRepository extends CrudRepository {}
public interface AddressRepository extends CrudRepository {}

2.3. リソースの作成

まず、使用するLibraryインスタンスを追加しましょう。

curl -i -X POST -H "Content-Type:application/json"
  -d '{"name":"My Library"}' http://localhost:8080/libraries

APIはJSONオブジェクトを返します。

{
  "name" : "My Library",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/libraries/1"
    },
    "library" : {
      "href" : "http://localhost:8080/libraries/1"
    },
    "address" : {
      "href" : "http://localhost:8080/libraries/1/libraryAddress"
    }
  }
}

Windowsでcurlを使用している場合は、JSON本体を表すString内の二重引用符をエスケープする必要があることに注意してください。

-d "{\"name\":\"My Library\"}"

応答本文で、関連付けリソースがlibraries/{libraryId}/addressエンドポイントで公開されていることがわかります。

アソシエーションを作成する前に、このエンドポイントにGETリクエストを送信すると、空のオブジェクトが返されます。

ただし、関連付けを追加する場合は、最初にAddressインスタンスも作成する必要があります。

curl -i -X POST -H "Content-Type:application/json"
  -d '{"location":"Main Street nr 5"}' http://localhost:8080/addresses

POSTリクエストの結果は、Addressレコードを含むJSONオブジェクトです。

{
  "location" : "Main Street nr 5",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/addresses/1"
    },
    "address" : {
      "href" : "http://localhost:8080/addresses/1"
    },
    "library" : {
      "href" : "http://localhost:8080/addresses/1/library"
    }
  }
}

2.4. アソシエーションの作成

After persisting both instances, we can establish the relationship by using one of the association resources

これは、text/uri-listのメディアタイプをサポートするHTTPメソッドPUTと、関連付けにバインドするリソースのURIを含む本体を使用して行われます。

Libraryエンティティは関連付けの所有者であるため、ライブラリに住所を追加しましょう。

curl -i -X PUT -d "http://localhost:8080/addresses/1"
  -H "Content-Type:text/uri-list" http://localhost:8080/libraries/1/libraryAddress

成功した場合、これはステータス204を返します。 確認するために、addresslibraryアソシエーションリソースを確認しましょう。

curl -i -X GET http://localhost:8080/addresses/1/library

これにより、“My Library”という名前のLibraryJSONオブジェクトが返されます。

remove an associationに対して、DELETEメソッドを使用してエンドポイントを呼び出すことができます。必ず、関係の所有者の関連付けリソースを使用してください。

curl -i -X DELETE http://localhost:8080/libraries/1/libraryAddress

3. 1対多の関係

1対多の関係は、@OneToManyおよび@ManyToOneアノテーションを使用して定義され、オプションの@RestResourceアノテーションを使用して関連付けリソースをカスタマイズできます。

3.1. データモデル

1対多の関係を例示するために、Libraryエンティティとの関係の「多」端を表す新しいBookエンティティを追加しましょう。

@Entity
public class Book {

    @Id
    @GeneratedValue
    private long id;

    @Column(nullable=false)
    private String title;

    @ManyToOne
    @JoinColumn(name="library_id")
    private Library library;

    // standard constructor, getter, setter
}

関係をLibraryクラスにも追加しましょう。

public class Library {

    //...

    @OneToMany(mappedBy = "library")
    private List books;

    //...

}

3.2. リポジトリ

また、BookRepositoryを作成する必要があります。

public interface BookRepository extends CrudRepository { }

3.3. 協会のリソース

add a book to a libraryを作成するには、最初に/booksコレクションリソースを使用してBookインスタンスを作成する必要があります。

curl -i -X POST -d "{\"title\":\"Book1\"}"
  -H "Content-Type:application/json" http://localhost:8080/books

そして、これがPOSTリクエストからのレスポンスです:

{
  "title" : "Book1",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/books/1"
    },
    "book" : {
      "href" : "http://localhost:8080/books/1"
    },
    "bookLibrary" : {
      "href" : "http://localhost:8080/books/1/library"
    }
  }
}

応答本文では、関連付けエンドポイント/books/{bookId}/libraryが作成されていることがわかります。

ライブラリリソースのURIを含むアソシエーションリソースにPUTリクエストを送信して、前のセクションで作成したassociate the book with the libraryを見てみましょう。

curl -i -X PUT -H "Content-Type:text/uri-list"
-d "http://localhost:8080/libraries/1" http://localhost:8080/books/1/library

ライブラリの/booksアソシエーションリソースでGETメソッドを使用すると、verify the books in the libraryを実行できます。

curl -i -X GET http://localhost:8080/libraries/1/books

返されるJSONオブジェクトには、books配列が含まれます。

{
  "_embedded" : {
    "books" : [ {
      "title" : "Book1",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/books/1"
        },
        "book" : {
          "href" : "http://localhost:8080/books/1"
        },
        "bookLibrary" : {
          "href" : "http://localhost:8080/books/1/library"
        }
      }
    } ]
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/libraries/1/books"
    }
  }
}

remove an associationに対して、アソシエーションリソースでDELETEメソッドを使用できます。

curl -i -X DELETE http://localhost:8080/books/1/library

4. 多対多の関係

多対多の関係は、@ManyToManyアノテーションを使用して定義され、それに@RestResourceを追加できます。

4.1. データモデル

多対多の関係の例を作成するために、Bookエンティティと多対多の関係を持つ新しいモデルクラスAuthorを追加しましょう。

@Entity
public class Author {

    @Id
    @GeneratedValue
    private long id;

    @Column(nullable = false)
    private String name;

    @ManyToMany(cascade = CascadeType.ALL)
    @JoinTable(name = "book_author",
      joinColumns = @JoinColumn(name = "book_id", referencedColumnName = "id"),
      inverseJoinColumns = @JoinColumn(name = "author_id",
      referencedColumnName = "id"))
    private List books;

    //standard constructors, getters, setters
}

Bookクラスにも関連付けを追加しましょう。

public class Book {

    //...

    @ManyToMany(mappedBy = "books")
    private List authors;

    //...
}

4.2. リポジトリ

Authorエンティティを管理するためのリポジトリインターフェイスを作成しましょう。

public interface AuthorRepository extends CrudRepository { }

4.3. 協会のリソース

前のセクションと同様に、関連付けを確立する前に、最初にcreate the resourcesを実行する必要があります。

まず、POSTリクエストを/authorsコレクションリソースに送信して、Authorインスタンスを作成しましょう。

curl -i -X POST -H "Content-Type:application/json"
  -d "{\"name\":\"author1\"}" http://localhost:8080/authors

次に、2番目のBookレコードをデータベースに追加しましょう。

curl -i -X POST -H "Content-Type:application/json"
  -d "{\"title\":\"Book 2\"}" http://localhost:8080/books

Authorレコードに対してGETリクエストを実行して、関連付けURLを表示してみましょう。

{
  "name" : "author1",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/authors/1"
    },
    "author" : {
      "href" : "http://localhost:8080/authors/1"
    },
    "books" : {
      "href" : "http://localhost:8080/authors/1/books"
    }
  }
}

これで、2つのBookレコードとAuthorレコードの間で、text/uri-listのメディアタイプをサポートするPUTメソッドでエンドポイントauthors/1/booksを使用してcreate an associationを実行できます。複数のURIを受け取ります。

複数のURIsを送信するには、それらを改行で区切る必要があります。

curl -i -X PUT -H "Content-Type:text/uri-list"
  --data-binary @uris.txt http://localhost:8080/authors/1/books

uris.txtファイルには、本のURIsがそれぞれ別々の行に含まれています。

http://localhost:8080/books/1
http://localhost:8080/books/2

verify both books have been associated with the authorに、GETリクエストをアソシエーションエンドポイントに送信できます。

curl -i -X GET http://localhost:8080/authors/1/books

そして、次の応答を受け取ります。

{
  "_embedded" : {
    "books" : [ {
      "title" : "Book 1",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/books/1"
        }
      //...
      }
    }, {
      "title" : "Book 2",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/books/2"
        }
      //...
      }
    } ]
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/authors/1/books"
    }
  }
}

remove an associationに、DELETEメソッドを使用してリクエストをアソシエーションリソースのURLに送信し、その後に{bookId}を送信できます。

curl -i -X DELETE http://localhost:8080/authors/1/books/1

5. TestRestTemplateを使用したエンドポイントのテスト

TestRestTemplateインスタンスを挿入し、使用する定数を定義するテストクラスを作成しましょう。

@RunWith(SpringRunner.class)
@SpringBootTest(classes = SpringDataRestApplication.class,
  webEnvironment = WebEnvironment.DEFINED_PORT)
public class SpringDataRelationshipsTest {

    @Autowired
    private TestRestTemplate template;

    private static String BOOK_ENDPOINT = "http://localhost:8080/books/";
    private static String AUTHOR_ENDPOINT = "http://localhost:8080/authors/";
    private static String ADDRESS_ENDPOINT = "http://localhost:8080/addresses/";
    private static String LIBRARY_ENDPOINT = "http://localhost:8080/libraries/";

    private static String LIBRARY_NAME = "My Library";
    private static String AUTHOR_NAME = "George Orwell";
}

5.1. 1対1の関係のテスト

コレクションリソースにPOSTリクエストを送信して、LibraryオブジェクトとAddressオブジェクトを保存する@Testメソッドを作成しましょう。

次に、関連リソースへのPUT要求との関係を保存し、同じリソースへのGET要求で確立されたことを確認します。

@Test
public void whenSaveOneToOneRelationship_thenCorrect() {
    Library library = new Library(LIBRARY_NAME);
    template.postForEntity(LIBRARY_ENDPOINT, library, Library.class);

    Address address = new Address("Main street, nr 1");
    template.postForEntity(ADDRESS_ENDPOINT, address, Address.class);

    HttpHeaders requestHeaders = new HttpHeaders();
    requestHeaders.add("Content-type", "text/uri-list");
    HttpEntity httpEntity
      = new HttpEntity<>(ADDRESS_ENDPOINT + "/1", requestHeaders);
    template.exchange(LIBRARY_ENDPOINT + "/1/libraryAddress",
      HttpMethod.PUT, httpEntity, String.class);

    ResponseEntity libraryGetResponse
      = template.getForEntity(ADDRESS_ENDPOINT + "/1/library", Library.class);
    assertEquals("library is incorrect",
      libraryGetResponse.getBody().getName(), LIBRARY_NAME);
}

5.2. 1対多の関係のテスト

Libraryインスタンスと2つのBookインスタンスを保存し、各Bookオブジェクトの/libraryアソシエーションリソースにPUTリクエストを送信し、それを検証する@Testメソッドを作成しましょう。関係が保存されました:

@Test
public void whenSaveOneToManyRelationship_thenCorrect() {
    Library library = new Library(LIBRARY_NAME);
    template.postForEntity(LIBRARY_ENDPOINT, library, Library.class);

    Book book1 = new Book("Dune");
    template.postForEntity(BOOK_ENDPOINT, book1, Book.class);

    Book book2 = new Book("1984");
    template.postForEntity(BOOK_ENDPOINT, book2, Book.class);

    HttpHeaders requestHeaders = new HttpHeaders();
    requestHeaders.add("Content-Type", "text/uri-list");
    HttpEntity bookHttpEntity
      = new HttpEntity<>(LIBRARY_ENDPOINT + "/1", requestHeaders);
    template.exchange(BOOK_ENDPOINT + "/1/library",
      HttpMethod.PUT, bookHttpEntity, String.class);
    template.exchange(BOOK_ENDPOINT + "/2/library",
      HttpMethod.PUT, bookHttpEntity, String.class);

    ResponseEntity libraryGetResponse =
      template.getForEntity(BOOK_ENDPOINT + "/1/library", Library.class);
    assertEquals("library is incorrect",
      libraryGetResponse.getBody().getName(), LIBRARY_NAME);
}

5.3. 多対多の関係のテスト

BookエンティティとAuthorエンティティ間の多対多の関係をテストするために、1つのAuthorレコードと2つのBookレコードを保存するテストメソッドを作成します。

次に、2つのBooksURIsを使用して/booksアソシエーションリソースにPUT要求を送信し、関係が確立されていることを確認します。

@Test
public void whenSaveManyToManyRelationship_thenCorrect() {
    Author author1 = new Author(AUTHOR_NAME);
    template.postForEntity(AUTHOR_ENDPOINT, author1, Author.class);

    Book book1 = new Book("Animal Farm");
    template.postForEntity(BOOK_ENDPOINT, book1, Book.class);

    Book book2 = new Book("1984");
    template.postForEntity(BOOK_ENDPOINT, book2, Book.class);

    HttpHeaders requestHeaders = new HttpHeaders();
    requestHeaders.add("Content-type", "text/uri-list");
    HttpEntity httpEntity = new HttpEntity<>(
      BOOK_ENDPOINT + "/1\n" + BOOK_ENDPOINT + "/2", requestHeaders);
    template.exchange(AUTHOR_ENDPOINT + "/1/books",
      HttpMethod.PUT, httpEntity, String.class);

    String jsonResponse = template
      .getForObject(BOOK_ENDPOINT + "/1/authors", String.class);
    JSONObject jsonObj = new JSONObject(jsonResponse).getJSONObject("_embedded");
    JSONArray jsonArray = jsonObj.getJSONArray("authors");
    assertEquals("author is incorrect",
      jsonArray.getJSONObject(0).getString("name"), AUTHOR_NAME);
}

6. 結論

このチュートリアルでは、Spring Data RESTとのさまざまなタイプの関係の使用を示しました。

例の完全なソースコードはover on GitHubにあります。