一對一關聯在 EJB 3 中支援兩種映射模式:外鍵模式(主表持有從表 FK)與 共享主鍵模式(從表 PK 同時為 FK)。本文以 Customer(客戶)與 Referee(推薦人)為例,完整示範雙向導航、外鍵自訂、共享主鍵配置與持久化差異。
id (PK) | name | referee_id (FK → t_referees.id)

id (PK) | name

@Entity
@Table(name = "t_customers")
public class Customer {
private int id;
private String name;
private Referee referee;
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
public int getId() { return id; }
// ... name getter/setter
@OneToOne // 預設外鍵名:referee_id (屬性名_id)
public Referee getReferee() { return referee; }
public void setReferee(Referee referee) { this.referee = referee; }
}
@Entity
@Table(name = "t_referees")
public class Referee {
private int id;
private String name;
private Customer customer;
@OneToOne(mappedBy = "referee") // 指向維護端屬性名
public Customer getCustomer() { return customer; }
public void setCustomer(Customer customer) { this.customer = customer; }
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
public int getId() { return id; }
// ... name getter/setter
}
若外鍵欄位非預設 referee_id 而是 referee1_id,兩種方式:
方式 1:修改屬性名(影響物件模型)
// Customer
private Referee referee1;
@OneToOne
public Referee getReferee1() { return referee1; }
// Referee
@OneToOne(mappedBy = "referee1")
public Customer getCustomer() { ... }
方式 2:@JoinColumn 覆蓋 (推薦,不影響物件模型)
@OneToOne
@JoinColumn(name = "referee1_id")
public Referee getReferee() { return referee; }
從表 t_referees.id 值依賴主表 t_customers.id,不再需要 referee_id 外鍵欄位。t_referees.id 移除自增。
@OneToOne(cascade = CascadeType.ALL) // 級聯持久化 Referee
@PrimaryKeyJoinColumn // 共享主鍵
public Referee getReferee() { return referee; }
@OneToOne
@PrimaryKeyJoinColumn
public Customer getCustomer() { return customer; }
@Id
@GeneratedValue(generator = "pkGenerator")
@GenericGenerator(
name = "pkGenerator",
strategy = "foreign",
parameters = @Parameter(name = "property", value = "customer")
)
public int getId() { return id; }
| 註解 | 作用 |
|---|---|
@PrimaryKeyJoinColumn | 雙方皆標記,聲明共享主鍵關聯 |
@GenericGenerator(strategy="foreign") | Hibernate 擴充,指定 ID 來源為關聯實體的屬性 |
@Parameter(name="property", value="customer") | 指向 Referee 端的關聯屬性名 |
cascade=ALL | Customer 持久化時自動持久化 Referee |
// 可獨立持久化 Referee
em.persist(referee);
customer.setReferee(referee);
em.persist(customer);
// 必須先設定關聯,再持久化 Customer (Referee 無法獨立持久化)
Customer customer = new Customer();
customer.setName("微軟");
Referee referee = new Referee();
referee.setName("趙軍");
customer.setReferee(referee);
em.persist(customer); // 容器自動持久化 Referee,ID 同步為 Customer ID
// 外鍵模式:可獨立查詢 Referee
System.out.println(em.find(Referee.class, 1).getCustomer().getName());
// 共享主鍵模式:PK 相同
Customer c = em.find(Customer.class, 1);
Referee r = em.find(Referee.class, 1); // 同一 ID
assert c.getId() == r.getId();
| 場景 | 推薦模式 | 理由 |
|---|---|---|
| 從表實體獨立存在、可能被多方引用 | 外鍵模式 | 獨立身分、可共享、支援獨立查詢 |
| 從表實體強依賴主表、生命週期綁定 | 共享主鍵 | 資料庫層級強制 1:1、節省 FK 欄位、查詢效能略優 |
| 既有資料表已有 FK 欄位 | 外鍵模式 | 零遷移成本 |
| 新系統設計、嚴格 1:1 組合關係 | 共享主鍵 | 領域模型更純粹、避免 FK 管理 |
t_referees.id 必須移除 AUTO_INCREMENT/IDENTITYcascade=ALL,否則 Referee 不會被持久化