1. 概述
本教程將教您如何使用 Spring Data REST 處理實體之間的關係。
我們將重點關注 Spring Data REST 提供的倉庫關聯資源,並考慮我們可以定義的所有關係類型。
為了避免額外的配置,我們將使用嵌入式 H2 數據庫進行示例。 您可以在《Spring Data REST 簡介》文章中找到所需的依賴項列表。
2. 一對一關係
2.1 數據模型
讓我們定義兩個實體類,<em >Library</em >(圖書館)和 <em >Address</em >(地址),通過使用 <em >@OneToOne</em > 註解建立一對一關係。 關聯關係由 <em >Library</em > 端擁有:
@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;如果在 Library 類中添加 secondaryAddress 屬性,我們將會同時存在兩個名為 address 的資源,從而產生衝突。
為了解決這個問題,我們可以通過為 rel 屬性指定不同的值,或者省略 RestResource 註解,使資源名稱默認設置為 secondaryAddress。
2.2. 倉庫
為了將這些實體暴露為資源,我們將為每個實體創建兩個倉庫接口,通過擴展 CrudRepository 接口:
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,則需要在 String 中轉義 JSON 實體中包含的雙引號字符:
-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/addressesPOST 請求的結果是一個 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. 創建關聯
在持久化了這兩個實例後,我們可以使用一個關聯資源來建立關係。
這通過使用 HTTP PUT 方法實現,該方法支持媒體類型 text/uri-list,並且包含一個 URI,該 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它應該返回 Library JSON 對象,名稱為 “My Library.”
要 移除關聯,我們可以調用該端點,使用 DELETE 方法,並確保使用關係的擁有者的關聯資源:
curl -i -X DELETE http://localhost:8080/libraries/1/libraryAddress3. 一對多關係
我們使用 @OneToMany 和 @ManyToOne 註解定義一對多關係。 還可以添加可選的 @RestResource 註解來自定義關聯資源。
3.1 數據模型
為了説明一對多關係,我們將添加一個新的 圖書實體,它代表了與 圖書館實體之間關係中的“多”的一端:
@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/library4. 多對多關係
我們使用 @ManyToMany 註解定義多對多關係,並且可以添加 @RestResource 註解。
4.1. 數據模型
為了創建一個多對多關係示例,我們將添加一個新的模型類,Author,它與Book實體存在多對多關係:
@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 請求,創建一個 作者 實例:
// 創建作者實例
// This code creates an Author instance.
// This is a sample code snippet to illustrate the process.
curl -i -X POST -H "Content-Type:application/json"
-d "{\"name\":\"author1\"}" http://localhost:8080/authors接下來,我們將向數據庫添加第二個 Book 記錄:
curl -i -X POST -H "Content-Type:application/json"
-d "{\"title\":\"Book 2\"}" http://localhost:8080/books然後,我們將執行一個 GET 請求,訪問我們的 Author 記錄以查看關聯 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"
}
}
}現在我們可以使用 PUT 方法,通過 endpoint authors/1/books 建立兩個 Book 記錄和 Author 記錄之間的關聯,該 endpoint 支持 media type text/uri-list 並可以接收多個 URI。
為了發送多個 URI,需要用換行符分隔:
curl -i -X PUT -H "Content-Type:text/uri-list"
--data-binary @uris.txt http://localhost:8080/authors/1/booksuris.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 後加上 {bookId} 發送帶有 DELETE 方法的請求:
curl -i -X DELETE http://localhost:8080/authors/1/books/12. 使用 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. 測試一對一關係
我們將創建一個 @Test 方法,通過向集合資源發送 POST 請求,保存 Library 和 Address 對象。
然後,它使用 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. 測試一對多關係
現在我們將創建一個 <em @Test</em> 方法,該方法保存一個 <em Library</em> 實例以及兩個 <em Book</em> 實例,然後向每個 <em Book</em> 對象的 <em /library</em> 關聯資源發送 PUT 請求,並驗證該關係是否已保存:
@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 實體之間的多對多關係,我們將創建一個測試方法,該方法保存一個 Author 記錄和兩個 Book 記錄。
然後,它向 /books 關聯資源發送 PUT 請求,其中包含兩個 Book 的 URI,並驗證關係已建立:
@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 的用法。