知識庫 / Spring / Spring Data RSS 訂閱

使用 Spring Data REST 處理關係

REST,Spring Data
HongKong
11
03:54 AM · Dec 06 ,2025

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/addresses

POST 請求的結果是一個 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/libraryAddress

3. 一對多關係

我們使用 @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/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;

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

2. 使用 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 請求,保存 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. 測試一對多關係

現在我們將創建一個 <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. 測試多對多關係

為了測試 BookAuthor 實體之間的多對多關係,我們將創建一個測試方法,該方法保存一個 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 的用法。

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

發佈 評論

Some HTML is okay.