在 Spring Data REST 中處理關係

REST,Spring Data
Remote
0
12:27 PM · Dec 01 ,2025

在本教程中,我們將學習

我們將重點關注 Spring Data REST 提供的倉庫的關聯資源,並考慮我們能夠定義的每種關係類型。

為了避免額外的設置,我們將使用

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:

@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.

. Otherwise, we’ll encounter a JsonMappingException with the message “Detected multiple association links with same relation type! Disambiguate association.”

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

@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.

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.

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

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

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

-d '{"name":"My Library"}' http://localhost:8080/libraries

Then the API returns the JSON object:

{
  "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:

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

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

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

-d '{"location":"Main Street nr 5"}' http://localhost:8080/addresses

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

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

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.

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

-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:

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

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

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

3. One-to-Many Relationship

我們定義一個一對多關係,使用@OneToMany@ManyToOne註解。 還可以添加可選的@RestResource註解來自定義關聯資源。

3.1. The Data Model

為了演示一對多關係,我們將添加一個新的Book實體,它代表“many”端與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. The Repository

我們也需要創建一個BookRepository:

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

3.3. The Association Resources

為了add a book to a library,我們需要首先使用/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請求associate the book with the library我們之前部分中創建的圖書館:

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

我們可以verify the books in the 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"
    }
  }
}

為了remove an association,我們可以使用在關聯資源上DELETE方法的:

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

4. Many-to-Many Relationship

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

4.1. The Data Model

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:

@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:

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

4.2. The Repository

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

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

4.3. The Association Resources

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:

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:

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:

 {
  "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.

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

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:

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:

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:

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

5. Testing the Endpoints With 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. Testing the One-to-One Relationship

我們將創建一個 @Test 方法,通過向集合資源發送 POST 請求來保存 LibraryAddress 對象:

然後,它通過向關聯資源的 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

現在,我們將創建一個 @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

對於測試 BookAuthor 實體之間的多對多關係,我們將創建一個測試方法,保存一個 Author 記錄和兩個 Book 記錄。

然後,它通過向 /books 關聯資源發送 PUT 請求,攜帶兩個 BookURI,並驗證該關係已建立:

@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 的不同類型關係。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.