Working with Relationships in Spring Data REST – 在Spring Data REST中处理关系

最后修改: 2017年 2月 28日

中文/混合/英文(键盘快捷键:t)

 1. Overview

1.概述

In this tutorial, we’ll learn how to work with relationships between entities in Spring Data REST.

在本教程中,我们将学习如何在Spring Data REST中处理实体之间的关系

We’ll focus on the association resources that Spring Data REST exposes for a repository, considering each type of relationship that we can define.

我们将专注于Spring Data REST为存储库暴露的关联资源,考虑我们可以定义的每一种关系类型。

To avoid any extra setup, we’ll use the H2 embedded database for the examples. We can find the list of required dependencies in our Introduction to Spring Data REST article.

为了避免任何额外的设置,我们将在示例中使用H2嵌入式数据库。我们可以在Spring Data REST 介绍文章中找到所需的依赖项列表。

2. One-to-One Relationship

2.一对一的关系

2.1. The Data Model

2.1.数据模型

Let’s define two entity classes, Library and Address, having a one-to-one relationship by using the @OneToOne annotation. The association is owned by the Library end of the association:

让我们定义两个实体类,LibraryAddress,通过使用@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;
    
    // 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
}

The @RestResource annotation is optional, and we can use it to customize the endpoint.

@RestResource注解是可选的,我们可以用它来定制端点。

We must also be careful to have different names for each association resource. Otherwise, we’ll encounter a JsonMappingException with the message “Detected multiple association links with same relation type! Disambiguate association.”

我们还必须注意为每个关联资源设置不同的名称。否则,我们将遇到一个JsonMappingException,其消息是“检测到具有相同关系类型的多个关联链接!消除关联。”

The association name defaults to the property name, and we can customize it using the rel attribute of the @RestResource annotation:

关联名称默认为属性名称,我们可以使用rel属性的@RestResource注解来定制它。

@OneToOne
@JoinColumn(name = "secondary_address_id")
@RestResource(path = "libraryAddress", rel="address")
private Address secondaryAddress;

If we were to add the secondaryAddress property above to the Library class, we’d have two resources named address, thus encountering a conflict.

如果我们将上面的secondaryAddress属性添加到Library类中,我们会有两个名为address的资源,从而遇到冲突。

We can resolve this by specifying a different value for the rel attribute, or by omitting the RestResource annotation so that the resource name defaults to secondaryAddress.

我们可以通过为rel属性指定一个不同的值,或者通过省略RestResource注解,使资源名称默认为secondaryAddress来解决这个问题。

2.2. The Repositories

2.2.存储库

In order to expose these entities as resources, we’ll create two repository interfaces for each of them by extending the CrudRepository interface:

为了将这些实体暴露为资源,我们将通过扩展CrudRepository接口为每个实体创建两个资源库接口。

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

2.3. Creating the Resources

2.3.创建资源

First, we’ll add a Library instance to work with:

首先,我们将添加一个Library实例来工作。

curl -i -X POST -H "Content-Type:application/json" 
  -d '{"name":"My Library"}' http://localhost:8080/libraries

Then the API returns the JSON object:

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

Note that if we’re using curl on Windows, we have to escape the double-quote character inside the String that represents the JSON body:

注意,如果我们在Windows上使用curl,我们必须转义代表JSON体的String里面的双引号字符。

-d "{\"name\":\"My Library\"}"

We can see in the response body that an association resource has been exposed at the libraries/{libraryId}/address endpoint.

我们可以在响应体中看到,在libraries/{libraryId}/address端点暴露了一个关联资源。

Before we create an association, sending a GET request to this endpoint will return an empty object.

在我们创建一个关联之前,向这个端点发送GET请求将返回一个空对象。

However, if we want to add an association, we must first create an Address instance:

然而,如果我们想添加一个关联,我们必须首先创建一个Address实例。

curl -i -X POST -H "Content-Type:application/json" 
  -d '{"location":"Main Street nr 5"}' http://localhost:8080/addresses

The result of the POST request is a JSON object containing the Address record:

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. Creating the Associations

2.4.创建关联

After persisting both instances, we can establish the relationship by using one of the association resources.

在持久化两个实例后,我们可以通过使用其中一个关联资源来建立关系

This is done using the HTTP method PUT, which supports a media type of text/uri-list, and a body containing the URI of the resource to bind to the association.

这是用HTTP方法PUT完成的,它支持text/uri-list的媒体类型,以及一个包含资源的URI的主体来绑定到关联。

Since the Library entity is the owner of the association, we’ll add an address to a library:

由于Library实体是关联的所有者,我们将向一个图书馆添加一个地址。

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

If successful, it’ll return status 204. To verify this, we can check the library association resource of the address:

如果成功,它将返回状态204。为了验证这一点,我们可以检查libraryaddress的关联资源。

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

It should return the Library JSON object with the name “My Library.”

它应该返回Library JSON对象,名称为“My Library”

To remove an association, we can call the endpoint with the DELETE method, making sure to use the association resource of the owner of the relationship:

为了删除一个关联,我们可以用DELETE方法调用端点,确保使用该关系所有者的关联资源。

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

3. One-to-Many Relationship

3.一对多的关系

We define a one-to-many relationship using the @OneToMany and @ManyToOne annotations. We can also add the optional @RestResource annotation to customize the association resource.

我们使用@OneToMany@ManyToOne注释来定义一对多的关系。我们还可以添加可选的@RestResource注解来定制关联资源。

3.1. The Data Model

3.1.数据模型

To exemplify a one-to-many relationship, we’ll add a new Book entity, which represents the “many” end of a relationship with the Library entity:

为了说明一对多的关系,我们将添加一个新的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
}

Then we’ll add the relationship to the Library class as well:

然后我们也要把这种关系添加到Library类。

public class Library {
 
    //...
 
    @OneToMany(mappedBy = "library")
    private List<Book> books;
 
    //...
 
}

3.2. The Repository

3.2.存储库

We also need to create a BookRepository:

我们还需要创建一个BookRepository

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

3.3. The Association Resources

3.3.协会的资源

In order to add a book to a library, we need to create a Book instance first by using the /books collection resource:

为了向图书馆添加一本书,我们需要首先通过使用/books集合资源创建一个Book实例。

curl -i -X POST -d "{\"title\":\"Book1\"}" 
  -H "Content-Type:application/json" http://localhost:8080/books

And here’s the response from the POST request:

这里是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"
    }
  }
}

In the response body, we can see that the association endpoint, /books/{bookId}/library, has been created.

在响应体中,我们可以看到关联端点/books/{bookId}/library,已经被创建。

Now let’s associate the book with the library we created in the previous section by sending a PUT request to the association resource that contains the URI of the library resource:

现在,让我们通过向包含图书馆资源的URI的关联资源发送一个PUT请求,将这本书与我们在上一节中创建的图书馆关联。

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

We can verify the books in the library by using the GET method on the library’s /books association resource:

我们可以通过对图书馆的/books关联资源使用GET方法来验证图书馆中的书籍

curl -i -X GET http://localhost:8080/libraries/1/books

The returned JSON object will contain a books array:

返回的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"
    }
  }
}

To remove an association, we can use the DELETE method on the association resource:

为了删除一个关联,我们可以在关联资源上使用DELETE方法。

curl -i -X DELETE http://localhost:8080/books/1/library

4. Many-to-Many Relationship

4.多对多的关系

We define a many-to-many relationship using the @ManyToMany annotation, to which we can also add @RestResource.

我们使用@ManyToMany注解来定义多对多的关系,我们还可以向其添加@RestResource

4.1. The Data Model

4.1.数据模型

To create an example of a many-to-many relationship, we’ll add a new model class, Author, which has a many-to-many relationship with the Book entity:

为了创建一个多对多关系的例子,我们将添加一个新的模型类,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
}

Then we’ll add the association in the Book class as well:

然后我们也要在Book类中添加关联。

public class Book {
 
    //...
 
    @ManyToMany(mappedBy = "books")
    private List<Author> authors;
 
    //...
}

4.2. The Repository

4.2.存储库

Next, we’ll create a repository interface to manage the Author entity:

接下来,我们将创建一个资源库接口来管理Author实体。

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

4.3. The Association Resources

4.3.协会的资源

As in the previous sections, we must first create the resources before we can establish the association.

和前面几节一样,我们必须首先创建资源,然后才能建立关联。

We’ll create an Author instance by sending a POST request to the /authors collection resource:

我们将通过向/authorscollection资源发送POST请求来创建一个Author实例。

curl -i -X POST -H "Content-Type:application/json" 
  -d "{\"name\":\"author1\"}" http://localhost:8080/authors

Next, we’ll add a second Book record to our database:

接下来,我们将向数据库添加第二条Book记录。

curl -i -X POST -H "Content-Type:application/json" 
  -d "{\"title\":\"Book 2\"}" http://localhost:8080/books

Then we’ll execute a GET request on our Author record to view the association URL:

然后,我们将在我们的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"
    }
  }
}

Now we can create an association between the two Book records and the Author record using the endpoint authors/1/books with the PUT method, which supports a media type of text/uri-list and can receive more than one URI.

现在我们可以在两个Book记录和Author记录之间创建一个关联,使用PUT方法的端点authors/1/books,它支持text/uri-list的媒体类型,可以接收一个以上的URI

To send multiple URIs, we have to separate them by a line break:

要发送多个URI,我们必须用换行符来分隔它们。

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

The uris.txt file contains the URIs of the books, each on a separate line:

uris.txt文件包含书籍的URIs,每个都在单独的一行。

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

To verify both books are associated with the author, we can send a GET request to the association endpoint:

为了验证这两本书都是与作者有关的,我们可以向关联端点发送一个GET请求。

curl -i -X GET http://localhost:8080/authors/1/books

And we’ll receive this response:

而我们会收到这样的回复。

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

To remove an association, we can send a request with the DELETE method to the URL of the association resource followed by {bookId}:

为了删除一个关联,我们可以用DELETE方法向关联资源的URL发送一个请求,后面加上{bookId}

curl -i -X DELETE http://localhost:8080/authors/1/books/1

5. Testing the Endpoints With TestRestTemplate

5.用TestRestTemplate测试端点

Let’s create a test class that injects a TestRestTemplate instance, and defines the constants we’ll use:

让我们创建一个测试类,注入一个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. Testing the One-to-One Relationship

5.1.测试一对一的关系

We’ll create a @Test method that saves Library and Address objects by making POST requests to the collection resources.

我们将创建一个@Test方法,通过向集合资源发出POST请求来保存LibraryAddress对象。

Then it saves the relationship with a PUT request to the association resource, and verifies that it’s been established with a GET request to the same resource:

然后,它通过对关联资源的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. Testing the One-to-Many Relationship

5.2.测试 “一对多 “的关系

Now we’ll create a @Test method that saves a Library instance and two Book instances, sends a PUT request to each Book object’s /library association resource, and verifies that the relationship has been saved:

现在,我们将创建一个@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. Testing the Many-to-Many Relationship

5.3.测试 “多对多 “关系

For testing the many-to-many relationship between Book and Author entities, we’ll create a test method that saves one Author record and two Book records.

为了测试BookAuthor实体之间的多对多关系,我们将创建一个测试方法,保存一条Author记录和两条Book记录。

Then it sends a PUT request to the /books association resource with the two BooksURIs, and verifies that the relationship has been established:

然后,它向带有两个BooksURIs的/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. Conclusion

6.结论

In this article, we demonstrated the use of different types of relationships with Spring Data REST.

在这篇文章中,我们演示了使用Spring Data REST的不同类型的关系。

The full source code of the examples can be found over on GitHub.

这些例子的完整源代码可以在GitHub上找到