データ]

  • リンク:/tag/spring-data-rest/[春のデータREST]


1概要

この記事では、Spring Data RESTでエンティティ間の関係をどのように扱うか** を見ていきます。

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

余分な設定を避けるために、例として

H2

組み込みデータベースを使用します。/spring-data-rest-intro[Spring Data RESTの紹介]の記事で、必要な依存関係のリストを見ることができます。


2一対一の関係


2.1. データモデル


@ OneToOne

アノテーションを使用して、一対一の関係を持つ2つのエンティティクラス

Library



Address

を定義しましょう。協会は協会の

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

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

  • 各関連リソースに対して異なる名前を付けるように注意しなければなりません** 。それ以外の場合は、次のメッセージを含む

    JsonMappingException

    が発生します。曖昧さのない関連」

関連名はデフォルトでプロパティ名になり、

@ 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. リポジトリ

これらのエンティティをリソースとして公開するには、

CrudRepository

インターフェイスを拡張して、それぞれに2つのリポジトリインターフェイスを作成します。

public interface LibraryRepository extends CrudRepository<Library, Long> {}

public interface AddressRepository extends CrudRepository<Address, Long> {}


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. 関連付けを作成する

  • 両方のインスタンスを永続化した後、アソシエーションリソースの1つを使用して関係を確立できます** 。

これはHTTPメソッドPUTを使用して行われます。これは

text/uri-list

のメディアタイプと、関連にバインドするリソースの

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が返されます。確認するには、

address



library

associationリソースを確認しましょう。

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

これにより、

“My Library”

という名前の

Library

JSONオブジェクトが返されるはずです。

アソシエーションを削除するには、関係の所有者のアソシエーションリソースを必ず使用して、DELETEメソッドでエンドポイントを呼び出します。

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


3一対多の関係

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<Book> books;

   //...

}


3.2. リポジトリ

また、

BookRepository

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

public interface BookRepository extends CrudRepository<Book, Long> { }


3.3. 協会のリソース

  • 本をライブラリに追加する** には、最初に/

    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リクエストを送信して、

本を前のセクションで作成したライブラリ

に関連付けます。

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

  • 図書館の/

    books

    アソシエーションリソースでGETメソッドを使用することで、** 図書館の本を確認することができます。

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"
    }
  }
}

アソシエーションを削除するには、アソシエーションリソースに対して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<Book> books;

   //standard constructors, getters, setters
}


Book

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

public class Book {

   //...

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

   //...
}


4.2. リポジトリ


Author

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

public interface AuthorRepository extends CrudRepository<Author, Long> { }


4.3. 協会のリソース

前のセクションと同様に、関連付けを確立する前に、まずリソースを作成する必要があります。

最初に/

authors

コレクションリソースにPOSTリクエストを送信して

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

関連付けURLを表示するには、

Author

レコードに対してGETリクエストを実行しましょう。

{
  "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"
    }
  }
}

これで、メディアタイプの

text/uri-list

をサポートし、複数の

URI

を受け取ることができるPUTメソッドを持つエンドポイント

authors/1/books

を使用して、2つの

Book

レコードと

Author

レコード間の関連付けを

作成

できます。

複数の

__URI

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

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


uris.txt

ファイルには、書籍の

__URI

__がそれぞれ別々の行に含まれています。

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

両方の本が著者に関連付けられていることを確認するために、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"
    }
  }
}

関連付けを削除するには、関連付けリソースのURLにDELETEメソッドを付けて

\ {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. 一対一の関係のテスト

コレクションリソースに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<String> httpEntity
      = new HttpEntity<>(ADDRESS__ENDPOINT + "/1", requestHeaders);
    template.exchange(LIBRARY__ENDPOINT + "/1/libraryAddress",
      HttpMethod.PUT, httpEntity, String.class);

    ResponseEntity<Library> libraryGetResponse
      = template.getForEntity(ADDRESS__ENDPOINT + "/1/library", Library.class);
    assertEquals("library is incorrect",
      libraryGetResponse.getBody().getName(), LIBRARY__NAME);
}


5.2. 一対多の関係をテストする


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<String> 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<Library> 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

レコードを保存するテストメソッドを作成します。

それから、それはPUTリクエストを2つのBookBookと一緒に、books関連リソースに送信し、関係が確立されたことを検証します。

@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<String> 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とのさまざまな種類の関係の使用方法を説明しました。

例の完全なソースコードはhttps://github.com/eugenp/tutorials/tree/master/spring-data-rest[over on GitHub]にあります。