1. 概述
本文将深入探讨 Spring Data REST 中实体关系的处理方式。我们将重点关注 Spring Data REST 为仓库暴露的关联资源,涵盖所有可定义的关系类型。
为简化环境配置,示例中使用 H2 嵌入式数据库。所需依赖列表可参考我们的 Spring Data REST 入门 文章。
2. 一对一关系
2.1. 数据模型
定义两个实体类 Library 和 Address,通过 @OneToOne 注解建立一对一关系。关联由 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;
// 标准构造器、getter/setter
}
@Entity
public class Address {
@Id
@GeneratedValue
private long id;
@Column(nullable = false)
private String location;
@OneToOne(mappedBy = "address")
private Library library;
// 标准构造器、getter/setter
}
@RestResource 注解是可选的,用于自定义接口。
⚠️ 务必确保每个关联资源名称唯一,否则会抛出 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;
若在 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 需转义双引号:
-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. 建立关联
持久化两个实例后,可通过关联资源建立关系。使用 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 关联资源验证:
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. 一对多关系
通过 @OneToMany 和 @ManyToOne 注解定义一对多关系,可添加可选的 @RestResource 自定义关联资源。
3.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;
// 标准构造器、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
通过 GET 请求图书馆的 /books 关联资源 验证书籍:
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. 数据模型
为展示多对多关系,添加新实体 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;
// 标准构造器、getter/setter
}
在 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
添加第二个 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 方法 创建两个 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"
}
}
}
解除关联 时,向关联资源 URL 后追加 {bookId} 并发送 DELETE 请求:
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. 测试一对一关系
创建 @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. 测试一对多关系
创建 @Test 方法:保存一个 Library 实例和两个 Book 实例,向每个 Book 对象的 /library 关联资源发送 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 关联资源发送包含两个书籍 URI 的 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 Data REST 中处理不同类型关系的方法。完整示例代码可在 GitHub 获取。