在先前的單元中,我們已經學會如何定義單一的 Customer
實體 (Entity)。然而,真實世界的應用程式中,資料很少是獨立存在的:
這些資料之間的連結,就是所謂的關聯 (Relationship)。
今天,我們將深入探討 Spring Data JPA 的核心精髓:實體關聯對映 (Entity Relationship Mapping, ERM)。我們將學習如何使用簡單的註解 (Annotation) 來定義實體間的關係,並讓 JPA (Java Persistence API) 自動為我們處理複雜的資料庫操作。
💡 小提醒:實體關聯就像是在告訴電腦「這些資料表之間是如何連接的」,就像建立家族關係圖一樣!
完成今日學習後,你將能夠:
@OneToOne
)、一對多 (@OneToMany
) 與多對多 (@ManyToMany
) 的實體關聯@JoinColumn
來定義外鍵 (Foreign Key),並徹底理解 mappedBy
在雙向關聯 (Bidirectional Relationship) 中的關鍵作用Customer => Order => OrderItem
) 的應用程式,並撰寫對應的 API🎓 學習小技巧:不用一次全部記住,先理解概念,再透過實作加深印象!
mappedBy
在深入探討各種關聯之前,我們必須先理解一個最重要的概念:關聯的擁有方 (Owning Side)。
想像一下,當兩個人要建立朋友關係時:
在資料庫中,兩張資料表的關聯是透過其中一張資料表的外鍵 (Foreign Key) 欄位來維護的。例如:
customers 表 allowed_domains 表
┌─────────┐ ┌─────────────┐
│ id (PK) │ ←────│ customer_id │ ← 這就是外鍵
│ name │ │ domain_name │
│ email │ │ id (PK) │
└─────────┘ └─────────────┘
JPA (Java Persistence API) 沿用了這個概念。在一個雙向關聯 (Bidirectional Relationship) 中,JPA 需要知道由哪一個實體來負責維護這個外鍵欄位。
擁有方 (Owning Side):
@JoinColumn
註解來明確指定外鍵欄位被擁有方 / 反向方 (Inverse Side):
mappedBy
屬性,告訴 JPA:「這段關係的控制權已經交給對方了,去對方那裡找 @JoinColumn
的設定」mappedBy
的值必須是「擁有方」實體中,代表此實體的那個屬性名稱
⚠️ 記住這個原則:
@JoinColumn
和mappedBy
總是成對出現,一個在擁有方,一個在反向方,而且它們永遠不會出現在同一個實體中。
簡單記憶法:
@JoinColumn
mappedBy
@OneToMany
) / 多對一 (@ManyToOne
) 關聯這是在應用程式中最常見的關聯。我們將以「一個 Customer
可以擁有多個 AllowedDomain
」這個範例來實作。
想像一個企業客戶管理系統:
google.com
、gmail.com
、youtube.com
等網域在資料庫層面,這意味著 allowed_domains
資料表中會有一個 customer_id
欄位作為外鍵:
customers 表 allowed_domains 表
┌─────────────┐ ┌─────────────────┐
│ id (PK) │←──────────────│ customer_id (FK)│
│ name │ │ domain_name │
│ email │ │ id (PK) │
│ status │ └─────────────────┘
└─────────────┘
1 多
因此,AllowedDomain
實體理所當然地成為了這段關係的擁有方 (Owning Side)。
import jakarta.persistence.*;
import java.util.UUID;
@Entity
@Table(name = "allowed_domains")
public class AllowedDomain {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@Column(name = "domain_name", nullable = false)
private String domainName;
/**
* 多對一關聯 (Many-to-One Relationship)
*
* 這裡的多方 (AllowedDomain) 是關聯的擁有方 (Owning Side)
*
* @ManyToOne: 標示多個 AllowedDomain 對應到一個 Customer
* @JoinColumn: 指定外鍵欄位
* - name = "customer_id": 在 allowed_domains 資料表中建立一個名為 customer_id 的外鍵欄位
* - nullable = false: 這個外鍵不能是空的,確保每個 domain 都必須屬於一個 customer
*
* FetchType.LAZY: 延遲載入 (Lazy Loading) - 只有在真正需要時才載入 Customer 資料
*/
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id", nullable = false)
private Customer customer;
// 建構子 (Constructors)
public AllowedDomain() {
}
public AllowedDomain(String domainName) {
this.domainName = domainName;
}
// Getters and Setters...
public UUID getId() {
return id;
}
public void setId(UUID id) {
this.id = id;
}
public String getDomainName() {
return domainName;
}
public void setDomainName(String domainName) {
this.domainName = domainName;
}
public Customer getCustomer() {
return customer;
}
public void setCustomer(Customer customer) {
this.customer = customer;
}
}
import jakarta.persistence.*;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
@Entity
@Table(name = "customers")
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
// ... 其他 name, email, status 欄位 ...
/**
* 一對多關聯 (One-to-Many Relationship)
*
* 這一方 (Customer) 是關聯的反向方 (Inverse Side)
*
* mappedBy = "customer":
* 告訴 JPA,這段關係的設定由 AllowedDomain 實體中的 "customer" 屬性負責
* JPA 會去 AllowedDomain.java 裡找那個 customer 欄位上的 @JoinColumn 設定
*
* cascade = CascadeType.ALL:
* 級聯操作 (Cascade Operations) - 當我們對 Customer 執行操作 (儲存、刪除) 時,
* 相關的 AllowedDomain 也會一併被操作。這在新增時非常方便!
*
* orphanRemoval = true:
* "孤兒移除" (Orphan Removal) - 如果一個 AllowedDomain 從這個 Set 集合中被移除,
* 那麼這個 AllowedDomain 在資料庫中也應該被刪除
*
* 使用 Set 而不是 List 可以避免重複資料,且效能較好
*/
@OneToMany(
mappedBy = "customer",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private Set<AllowedDomain> allowedDomains = new HashSet<>();
/**
* 🔧 Helper method 輔助方法 - 用來同步雙向關聯
* 這個方法確保當我們建立關聯時,雙方都能正確設定
*/
public void addDomain(AllowedDomain domain) {
allowedDomains.add(domain); // 將 domain 加入到 Customer 的集合中
domain.setCustomer(this); // 設定 domain 的 customer 屬性
}
public void removeDomain(AllowedDomain domain) {
allowedDomains.remove(domain); // 從 Customer 的集合中移除 domain
domain.setCustomer(null); // 清除 domain 的 customer 屬性
}
// Getters and Setters...
public Set<AllowedDomain> getAllowedDomains() {
return allowedDomains;
}
public void setAllowedDomains(Set<AllowedDomain> allowedDomains) {
this.allowedDomains = allowedDomains;
}
}
"customer"
必須與 AllowedDomain 實體中的屬性名稱完全一致💡 為什麼需要 Helper Methods?
在雙向關聯中,我們需要確保兩邊的資料都保持同步。如果只設定一邊,可能會造成資料不一致的問題。
現在,讓我們將理論付諸實踐!
AllowedDomain
Entity請建立 com.example.demo.entities.AllowedDomain.java
檔案,內容如上面的程式碼範例。
💡 專案結構提醒:確保檔案放在正確的套件路徑下
Customer
Entity請更新您原有的 Customer.java
,加入 @OneToMany
的 allowedDomains
集合,內容如上面的程式碼範例。
AllowedDomainRepository
建立資料存取層 (Data Access Layer):
package com.example.demo.repositories;
import com.example.demo.entities.AllowedDomain;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.UUID;
@Repository
public interface AllowedDomainRepository extends JpaRepository<AllowedDomain, UUID> {
// JpaRepository 已經提供了基本的 CRUD 操作
// 如需要自訂查詢方法,可以在這裡添加
}
在您的應用程式啟動類別中加入 CommandLineRunner
來測試:
package com.example.demo;
import com.example.demo.entities.AllowedDomain;
import com.example.demo.entities.Customer;
import com.example.demo.enums.CustomerStatus;
import com.example.demo.repositories.CustomerRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
/**
* 🧪 測試資料建立
* CommandLineRunner 會在應用程式啟動後自動執行
*/
@Bean
public CommandLineRunner commandLineRunner(CustomerRepository customerRepository) {
return args -> {
// 建立一個新客戶
Customer customer = new Customer();
customer.setName("Gogoro Taiwan");
customer.setEmail("contact@gogoro.com");
customer.setStatus(CustomerStatus.ACTIVE);
// 建立允許的網域,並加入到客戶中
AllowedDomain domain1 = new AllowedDomain();
domain1.setDomainName("gogoro.com");
customer.addDomain(domain1); // 🔧 使用 helper method 來確保雙向關聯同步
AllowedDomain domain2 = new AllowedDomain();
domain2.setDomainName("gogoro.tw");
customer.addDomain(domain2);
// 儲存客戶
// 由於設定了 CascadeType.ALL,JPA 會自動一併儲存兩個 AllowedDomain
customerRepository.save(customer);
System.out.println("客戶與允許的網域已成功儲存!");
System.out.println("客戶名稱: " + customer.getName());
System.out.println("網域數量: " + customer.getAllowedDomains().size());
};
}
}
啟動應用程式後,您會看到:
customers
和 allowed_domains
兩張資料表被自動建立🔍 檢查方式:可以透過資料庫管理工具查看資料表結構和資料
真實世界的關聯往往是多層的。讓我們擴充專案,模擬一個電商客戶下訂單的場景。
想像一個線上購物系統:
Customer (客戶)
↓ 一對多
Order (訂單)
↓ 一對多
OrderItem (訂單品項)
資料表關係:
customers orders order_items
┌─────────┐ ┌─────────────┐ ┌─────────────┐
│ id (PK) │←─────│ customer_id │ │ order_id │
│ name │ │ id (PK) │←────│ product_name│
│ email │ │ order_date │ │ quantity │
│ status │ │ total_amount│ │ price │
└─────────┘ └─────────────┘ │ id (PK) │
└─────────────┘
package com.example.demo.entities;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@Column(nullable = false)
private LocalDateTime orderDate;
@Column(name = "total_amount", precision = 10, scale = 2)
private BigDecimal totalAmount;
/**
* 多對一關聯 - Order 與 Customer
* 擁有方:Order 擁有 customer_id 外鍵
*
* 一個訂單只能屬於一個客戶,但一個客戶可以有多個訂單
*/
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id", nullable = false)
private Customer customer;
/**
* 一對多關聯 - Order 與 OrderItem
* 反向方:OrderItem 擁有 order_id 外鍵
*
* 一個訂單可以有多個品項
*/
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<OrderItem> items = new HashSet<>();
// 建構子
public Order() {
this.orderDate = LocalDateTime.now();
this.totalAmount = BigDecimal.ZERO;
}
// Helper methods 輔助方法
public void addItem(OrderItem item) {
items.add(item);
item.setOrder(this);
calculateTotalAmount(); // 重新計算總金額
}
public void removeItem(OrderItem item) {
items.remove(item);
item.setOrder(null);
calculateTotalAmount(); // 重新計算總金額
}
/**
* 計算訂單總金額
* 根據所有訂單品項的小計來計算
*/
public void calculateTotalAmount() {
this.totalAmount = items.stream()
.map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
// Getters and Setters...
}
package com.example.demo.entities;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.util.UUID;
@Entity
@Table(name = "order_items")
public class OrderItem {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@Column(name = "product_name", nullable = false)
private String productName;
@Column(nullable = false)
private int quantity;
@Column(precision = 10, scale = 2, nullable = false)
private BigDecimal price;
/**
* 多對一關聯 - OrderItem 與 Order
* 擁有方:OrderItem 擁有 order_id 外鍵
*
* 一個訂單品項只能屬於一個訂單
*/
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id", nullable = false)
private Order order;
// 建構子
public OrderItem() {
}
public OrderItem(String productName, int quantity, BigDecimal price) {
this.productName = productName;
this.quantity = quantity;
this.price = price;
}
/**
* 計算此品項的小計
* @return 價格 × 數量
*/
public BigDecimal getSubtotal() {
return price.multiply(BigDecimal.valueOf(quantity));
}
// Getters and Setters...
}
透過這種方式,我們就建立了一個清晰的三層結構:
// 從客戶查詢所有訂單
Customer customer = customerRepository.findById(customerId).orElse(null);
Set<Order> customerOrders = customer.getOrders(); // 取得客戶的所有訂單
// 從訂單查詢所有品項
Order order = orderRepository.findById(orderId).orElse(null);
Set<OrderItem> orderItems = order.getItems(); // 取得訂單的所有品項
// 一次性查詢:從客戶到訂單品項
customer.getOrders() // 取得所有訂單
.stream()
.flatMap(o -> o.getItems().stream()) // 取得所有訂單的所有品項
.forEach(System.out::println); // 印出每個品項
💡 實務應用:這種設計在電商、訂單管理、庫存系統中非常常見!
@OneToOne
) 關聯🎯 應用情境:一個 User
(使用者) 只能有一個 UserProfile
(使用者檔案)。
這與一對多的實作非常相似,通常也是使用外鍵策略 (Foreign Key Strategy)。
User.java (反向方 - Inverse Side)
@Entity
public class User {
@Id
private Long id;
private String username;
/**
* 一對一關聯 - User 與 UserProfile
* 反向方:UserProfile 擁有 user_id 外鍵
*/
@OneToOne(mappedBy = "user", cascade = CascadeType.ALL)
private UserProfile userProfile;
// Getters and Setters...
}
UserProfile.java (擁有方 - Owning Side)
@Entity
public class UserProfile {
@Id
private Long id;
private String firstName;
private String lastName;
private String phoneNumber;
/**
* 一對一關聯 - UserProfile 與 User
* 擁有方:UserProfile 擁有 user_id 外鍵
*/
@OneToOne
@JoinColumn(name = "user_id")
private User user;
// Getters and Setters...
}
@ManyToMany
) 關聯 - 最佳實踐🎯 應用情境:一個 Product
(產品) 可以有多個 Tag
(標籤),一個 Tag
也可以被用於多個 Product
。
雖然 JPA 提供了 @ManyToMany
註解,但它會隱藏中介資料表 (Join Table):
// ❌ 不建議使用,因為無法擴充
@Entity
public class Product {
@ManyToMany
@JoinTable(
name = "product_tags",
joinColumns = @JoinColumn(name = "product_id"),
inverseJoinColumns = @JoinColumn(name = "tag_id")
)
private Set<Tag> tags = new HashSet<>();
}
❌ 問題:
業界最佳實踐是:將多對多關係,手動拆解成兩個「一對多」關係。
ProductTag
ProductTag
與 Product
建立多對一關聯ProductTag
與 Tag
建立多對一關聯Product.java
@Entity
public class Product {
@Id
private Long id;
private String name;
// 一對多:Product → ProductTag
@OneToMany(mappedBy = "product", cascade = CascadeType.ALL)
private Set<ProductTag> productTags = new HashSet<>();
}
Tag.java
@Entity
public class Tag {
@Id
private Long id;
private String name;
// 一對多:Tag → ProductTag
@OneToMany(mappedBy = "tag", cascade = CascadeType.ALL)
private Set<ProductTag> productTags = new HashSet<>();
}
中介實體:ProductTag.java
```java
@Entity
@Table(name = "product_tags")
public class ProductTag {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 多對一:ProductTag → Product (擁有方)
@ManyToOne
@JoinColumn(name = "product_id")
private Product product;
// 多對一:ProductTag → Tag (擁有方)
@ManyToOne
@JoinColumn(name = "tag_id")
private Tag tag;
// 額外屬性 - 這就是使用中介實體的優勢!
@Column(name = "tagged_at")
private LocalDateTime taggedAt;
@Column(name = "tagged_by")
private String taggedBy;
// 建構子
public ProductTag(Product product, Tag tag, String taggedBy) {
this.product = product;
this.tag = tag;
this.taggedBy = taggedBy;
this.taggedAt = LocalDateTime.now();
}
}
💡 記住:在企業級應用中,幾乎所有的多對多關係最終都會需要額外的屬性,所以一開始就使用中介實體是明智的選擇!
當我們使用 @JoinColumn
時,JPA (Java Persistence API) 透過 Hibernate 會在產生 DDL (Data Definition Language) 語法時,為 PostgreSQL 資料庫加上外鍵約束 (Foreign Key Constraint)。
以我們的 allowed_domains
和 customers
資料表為例:
-- customers 資料表
CREATE TABLE customers (
id UUID PRIMARY KEY,
name VARCHAR(50) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
status VARCHAR(20) NOT NULL
);
-- allowed_domains 資料表
CREATE TABLE allowed_domains (
id UUID PRIMARY KEY,
domain_name VARCHAR(255) NOT NULL,
customer_id UUID NOT NULL,
-- 外鍵約束
CONSTRAINT fk_allowed_domains_customer
FOREIGN KEY (customer_id) REFERENCES customers(id)
);
這個約束會建立一個從 allowed_domains.customer_id
指向 customers.id
的外鍵關係。
這個約束由資料庫層級來強制執行,帶來了以下重要好處:
PostgreSQL 會確保 allowed_domains.customer_id
的值,必須是 customers
資料表中一個真實存在的 id
。
-- ✅ 這個操作會成功 (假設 customer_id 存在)
INSERT INTO allowed_domains (id, domain_name, customer_id)
VALUES ('550e8400-e29b-41d4-a716-446655440000', 'example.com', '已存在的客戶ID');
-- ❌ 這個操作會失敗
INSERT INTO allowed_domains (id, domain_name, customer_id)
VALUES ('550e8400-e29b-41d4-a716-446655440001', 'fake.com', '不存在的客戶ID');
-- 錯誤: 違反外鍵約束 "fk_allowed_domains_customer"
好處:從根本上杜絕了「孤兒資料 (Orphaned Data)」的產生!
如果有人試圖直接在資料庫刪除一個底下還有訂單的客戶,PostgreSQL 會拒絕這個操作:
-- ❌ 這個操作會失敗 (如果該客戶有相關的 allowed_domains)
DELETE FROM customers WHERE id = '有相關資料的客戶ID';
-- 錯誤: 違反外鍵約束,無法刪除被參照的資料
好處:有效保護您的資料,避免因為誤操作而導致資料不一致!
CascadeType
@OneToMany(
mappedBy = "customer",
cascade = CascadeType.ALL, // 🔧 應用程式層級的級聯操作
orphanRemoval = true
)
private Set<AllowedDomain> allowedDomains = new HashSet<>();
CONSTRAINT fk_allowed_domains_customer
FOREIGN KEY (customer_id) REFERENCES customers(id)
應用程式層級 (JPA)
↓
最後防線 (PostgreSQL 外鍵約束)
↓
資料庫
這種雙重保護機制確保了:
💡 最佳實踐:永遠同時使用 JPA 的級聯操作和資料庫的外鍵約束,這樣可以獲得最好的開發體驗和資料安全性!
最後,我們來完成「查詢某客戶的所有 AllowedDomain」的 REST API。
CustomerRepository
新增查詢方法雖然我們可以透過 findById
拿到 Customer 再 getAllowedDomains
,但直接查詢能更精確地表達意圖。
package com.example.demo.repositories;
import com.example.demo.entities.AllowedDomain;
import com.example.demo.entities.Customer;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.Set;
import java.util.UUID;
@Repository
public interface CustomerRepository extends JpaRepository<Customer, UUID> {
/**
* 使用 JPQL 查詢特定客戶的所有允許網域
*
* @param customerId 客戶 ID
* @return 該客戶的所有允許網域
*/
@Query("SELECT c.allowedDomains FROM Customer c WHERE c.id = :customerId")
Set<AllowedDomain> findAllowedDomainsByCustomerId(@Param("customerId") UUID customerId);
}
💡 JPQL 說明:這是 Java Persistence Query Language,是 JPA 的查詢語言,語法類似 SQL 但操作的是實體物件而不是資料表。
package com.example.demo.services;
import com.example.demo.entities.AllowedDomain;
import com.example.demo.entities.Customer;
import com.example.demo.repositories.CustomerRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
/**
* Service 層:業務邏輯處理
* 負責處理複雜的業務邏輯,並協調多個 Repository
*/
@Service
public class CustomerService {
@Autowired
private CustomerRepository customerRepository;
/**
* 取得特定客戶的所有允許網域
*
* @param customerId 客戶 ID
* @return 該客戶的所有允許網域
*/
public Set<AllowedDomain> getDomainsForCustomer(UUID customerId) {
return customerRepository.findAllowedDomainsByCustomerId(customerId);
}
/**
* 根據 ID 查詢客戶
*
* @param customerId 客戶 ID
* @return 客戶資料 (如果存在)
*/
public Optional<Customer> findCustomerById(UUID customerId) {
return customerRepository.findById(customerId);
}
/**
* 儲存客戶
*
* @param customer 要儲存的客戶
* @return 儲存後的客戶
*/
public Customer saveCustomer(Customer customer) {
return customerRepository.save(customer);
}
}
package com.example.demo.controllers;
import com.example.demo.entities.AllowedDomain;
import com.example.demo.entities.Customer;
import com.example.demo.services.CustomerService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
/**
* Controller 層:API 端點
* 負責處理 HTTP 請求和回應
*/
@RestController
@RequestMapping("/api/customers")
public class CustomerController {
@Autowired
private CustomerService customerService;
/**
* 取得特定客戶的所有允許網域
*
* GET /api/customers/{customerId}/domains
*
* @param customerId 客戶 ID (從 URL 路徑中取得)
* @return 該客戶的所有允許網域
*/
@GetMapping("/{customerId}/domains")
public ResponseEntity<Set<AllowedDomain>> getCustomerDomains(@PathVariable UUID customerId) {
// 先檢查客戶是否存在
Optional<Customer> customer = customerService.findCustomerById(customerId);
if (customer.isEmpty()) {
return ResponseEntity.notFound().build(); // 404 Not Found
}
// 取得客戶的允許網域
Set<AllowedDomain> domains = customerService.getDomainsForCustomer(customerId);
if (domains.isEmpty()) {
return ResponseEntity.noContent().build(); // 204 No Content
}
return ResponseEntity.ok(domains); // 200 OK
}
/**
* 根據 ID 取得客戶資料
*
* GET /api/customers/{customerId}
*
* @param customerId 客戶 ID
* @return 客戶資料
*/
@GetMapping("/{customerId}")
public ResponseEntity<Customer> getCustomer(@PathVariable UUID customerId) {
Optional<Customer> customer = customerService.findCustomerById(customerId);
return customer.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
}
現在,啟動您的應用程式,並使用以下方式測試:
http://localhost:8080/api/customers/{您儲存的客戶ID}/domains
GET http://localhost:8080/api/customers/{客戶ID}/domains
Accept: application/json
curl -X GET "http://localhost:8080/api/customers/{客戶ID}/domains" \
-H "Accept: application/json"
您應該會看到類似以下的 JSON 回應:
[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"domainName": "gogoro.com"
},
{
"id": "550e8400-e29b-41d4-a716-446655440001",
"domainName": "gogoro.tw"
}
]
恭喜! 現在已經成功建立了一個包含完整關聯對映的 Spring Data JPA 應用程式,並提供了對應的 REST API!
@JoinColumn
mappedBy
@OneToOne
:一對一@OneToMany
/ @ManyToOne
:一對多 / 多對一@ManyToMany
:多對多 (建議拆解為兩個一對多)註解 | 用途 | 範例 |
---|---|---|
@ManyToOne |
多對一關聯 (擁有方) | AllowedDomain -> Customer |
@OneToMany |
一對多關聯 (反向方) | Customer -> AllowedDomain |
@JoinColumn |
指定外鍵欄位 | @JoinColumn(name = "customer_id") |
mappedBy |
指定關聯的屬性名 | mappedBy = "customer" |
CascadeType.ALL |
級聯所有操作 | 儲存、更新、刪除一併執行 |
orphanRemoval |
孤兒移除 | 從集合移除時自動刪除 |
FetchType.LAZY |
延遲載入 | 只有在需要時才載入關聯資料 |