1. 概要

このチュートリアルでは、 Spring DataRESTでエンティティ間の関係を操作する方法を学習します。

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

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

2. 1対1の関係

2.1. データモデル

@OneToOne アノテーションを使用して、1対1の関係を持つLibraryAddressの2つのエンティティクラスを定義しましょう。 アソシエーションは、アソシエーションの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 アノテーションを省略して、リソース名がデフォルトでsecondsaryAddressになるようにします。

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つを使用して関係を確立できます。

これは、 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が返されます。 これを確認するために、アドレスライブラリアソシエーションリソースを確認できます。

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

LibraryJSONオブジェクトを「MyLibrary」という名前で返す必要があります。

アソシエーションを削除するには、DELETEメソッドを使用してエンドポイントを呼び出すことができます。必ず、リレーションシップの所有者のアソシエーションリソースを使用してください。

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

3. 1対多の関係

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

3.1. データモデル

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

@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

次に、 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"
    }
  }
}

これで、エンドポイント authors / 1 / books とPUTを使用して、2つのBookレコードとAuthorレコードの間に関連付けを作成できます。 text / uri-list のメディアタイプをサポートし、複数のURIを受信できるメソッド。

複数の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"
    }
  }
}

アソシエーションを削除するには、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<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. 1対多の関係のテスト

次に、Libraryインスタンスと2つのBookインスタンスを保存し、各BookにPUTリクエストを送信する@Testメソッドを作成します。オブジェクトの/library 関連付けリソース、および関係が保存されていることを確認します。

@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レコード。

次に、2つの Books URIを使用して/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<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 DataRESTとのさまざまなタイプの関係の使用方法を示しました。

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