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

データ]

  • リンク:/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]にあります。