Java 持久化/一對一
Java 中的 一對一 關係是指源物件有一個屬性引用另一個目標物件,並且(如果)該目標物件具有反向關係回到源物件,則它也是一個 一對一 關係。Java 和 JPA 中的所有關係都是單向的,也就是說,如果源物件引用目標物件,則不能保證目標物件也與源物件有關係。這與關係資料庫不同,在關係資料庫中,關係是透過外部索引鍵和查詢來定義的,從而使反向查詢始終存在。
JPA 還定義了 多對一 關係,它類似於 一對一 關係,只是反向關係(如果定義)是 一對多 關係。JPA 中 一對一 和 多對一 關係的主要區別在於 多對一 始終包含從源物件表到目標物件表的外部索引鍵,而 一對一 關係中的外部索引鍵可能位於源物件表或目標物件表中。如果外部索引鍵位於目標物件表中,JPA 要求關係必須是雙向的(必須在兩個物件中定義),並且源物件必須使用 mappedBy 屬性來定義對映。
在 JPA 中,一對一 關係是透過 @OneToOne 註釋或 <one-to-one> 元素來定義的。一對一 關係通常需要 @JoinColumn 或 @JoinColumns(如果使用複合主鍵)。
EMPLOYEE(表)
| EMP_ID | FIRSTNAME | LASTNAME | SALARY | ADDRESS_ID |
| 1 | Bob | Way | 50000 | 6 |
| 2 | Sarah | Smith | 60000 | 7 |
ADDRESS(表)
| ADDRESS_ID | STREET | CITY | PROVINCE | COUNTRY | P_CODE |
| 6 | 17 Bank St | Ottawa | ON | Canada | K2H7Z5 |
| 7 | 22 Main St | Toronto | ON | Canada | L5H2D5 |
@Entity
public class Employee {
@Id
@Column(name="EMP_ID")
private long id;
...
@OneToOne(fetch=FetchType.LAZY)
@JoinColumn(name="ADDRESS_ID")
private Address address;
}
<entity name="Employee" class="org.acme.Employee" access="FIELD">
<attributes>
<id name="id">
<column name="EMP_ID"/>
</id>
<one-to-one name="address" fetch="LAZY">
<join-column name="ADDRESS_ID"/>
</one-to-one>
</attributes>
</entity>
一對一 關係的典型面向物件視角使資料模型映象物件模型,即源物件有一個指向目標物件的指標,因此資料庫源表有一個指向目標表的外部索引鍵。但這並不總是資料庫的工作方式,事實上,許多資料庫開發人員認為在目標表中擁有外部索引鍵是合理的,因為這強制了 一對一 關係的唯一性。我個人更喜歡面向物件的視角,但是你很可能遇到兩種情況。
要開始考慮雙向的 一對一 關係,你不需要兩個外部索引鍵,每個表一個,因此在關係的擁有方有一個外部索引鍵就足夠了。在 JPA 中,反向一對一 必須使用 mappedBy 屬性(有一些例外),這使得 JPA 提供程式使用源對映中的外部索引鍵和對映資訊來定義目標對映。
另請參閱 目標外部索引鍵、主鍵連線列、級聯主鍵。
以下給出了反向 address 關係的樣子示例。
@Entity
public class Address {
@Id
@Column(name = "ADDRESS_ID")
private long id;
...
@OneToOne(fetch=FetchType.LAZY, mappedBy="address")
private Employee owner;
...
}
<entity name="Address" class="org.acme.Address" access="FIELD">
<attributes>
<id name="id"/>
<one-to-one name="owner" fetch="LAZY" mapped-by="address"/>
</attributes>
</entity>
- 如果你在兩個不同的對映中使用同一個欄位,通常需要使用
insertable, updatable = false將其中一個設定為只讀。 - 參見目標外部索引鍵、主鍵連線列、級聯主鍵。
- 這通常是由於你在
OneToOne關係中錯誤地映射了外部索引鍵導致的。
- 如果你的JPA提供者不支援引用完整性,或者無法解析雙向約束,也會出現這種情況。在這種情況下,你可能需要刪除約束,或者使用
EntityManagerflush()來確保寫入物件的順序。
- 確保你設定了物件的
OneToOne的值,如果OneToOne是雙向OneToOne關係的一部分,確保你在兩個物件中都設定了OneToOne,JPA不會為你維護雙向關係。 - 還要檢查你是否正確定義了
JoinColumn,確保你沒有設定insertable, updatable = false,或者使用PrimaryKeyJoinColumn或mappedBy。
如果OneToOne關係使用目標外部索引鍵(外部索引鍵位於目標表,而不是源表),那麼JPA要求你在兩個方向都定義OneToOne對映,並且目標外部索引鍵對映使用mappedBy屬性。這樣做的原因是,源物件中的對映隻影響JPA寫入源表的行,如果外部索引鍵位於目標表,JPA就無法輕鬆寫入這個欄位。
但是,還有其他方法可以解決這個問題。在JPA中,JoinColumn定義了insertable和updatable屬性,這些屬性可以用來指示JPA提供者外部索引鍵實際上位於目標物件的表中。啟用這些屬性後,JPA不會向源表寫入任何內容,大多數JPA提供者也會推斷外部索引鍵約束位於目標表中,以在插入時保持引用完整性。JPA還定義了@PrimaryKeyJoinColumn,可用於定義相同的內容。但是,你仍然需要以某種方式對映目標物件中的外部索引鍵,但可以使用Basic對映來完成此操作。
一些JPA提供者可能支援針對目標外部索引鍵的單向OneToOne對映選項。
目標外部索引鍵可能難以理解,因此你可能需要閱讀本節兩次。但它們可能變得更加複雜。如果你的資料模型級聯主鍵,那麼你最終可能會得到一個只有一個邏輯外部索引鍵的OneToOne,但其中包含一些邏輯上是目標外部索引鍵的欄位。
例如,考慮Company、Department、Employee。Company的id是COM_ID,Department的id是COM_ID和DEPT_ID的組合主鍵,Employee的id是COM_ID、DEP_ID和EMP_ID的組合主鍵。因此,對於一個Employee,它與company的關係使用一個普通的ManyToOne,帶有一個外部索引鍵,但它與department的關係使用一個ManyToOne,帶有一個外部索引鍵,但是COM_ID使用insertable, updatable = false或PrimaryKeyJoinColumn,因為它實際上是透過company關係對映的。Employee與其address的關係使用一個普通的ADD_ID外部索引鍵,但對COM_ID、DEP_ID和EMP_ID使用一個目標外部索引鍵。
這在某些JPA提供者中可能有效,其他提供者可能需要不同的配置,或者不支援這種型別的資料模型。
COMPANY(表)
| COM_ID | NAME |
| 1 | ACME |
| 2 | Wikimedia |
DEPARTMENT(表)
| COM_ID | DEP_ID | NAME |
| 1 | 1 | Billing |
| 1 | 2 | Research |
| 2 | 1 | Accounting |
| 2 | 2 | Research |
EMPLOYEE(表)
| COM_ID | DEP_ID | EMP_ID | NAME | MNG_ID | ADD_ID |
| 1 | 1 | 1 | Bob Way | null | 1 |
| 1 | 1 | 2 | Joe Smith | 1 | 2 |
| 1 | 2 | 1 | Sarah Way | null | 1 |
| 1 | 2 | 2 | John Doe | 1 | 2 |
| 2 | 1 | 1 | Jane Doe | null | 1 |
| 2 | 2 | 1 | Alice Smith | null | 1 |
ADDRESS(表)
| COM_ID | DEP_ID | ADD_ID | ADDRESS |
| 1 | 1 | 1 | 17 Bank, Ottawa, ONT |
| 1 | 1 | 2 | 22 Main, Ottawa, ONT |
| 1 | 2 | 1 | 255 Main, Toronto, ONT |
| 1 | 2 | 2 | 12 Main, Winnipeg, MAN |
| 2 | 1 | 1 | 72 Riverside, Winnipeg, MAN |
| 2 | 2 | 1 | 82 Riverside, Winnipeg, MAN |
@Entity
@IdClass(EmployeeId.class)
public class Employee {
@Id
@Column(name="EMP_ID")
private long employeeId;
@Id
@Column(name="DEP_ID", insertable=false, updatable=false)
private long departmentId;
@Id
@Column(name="COM_ID", insertable=false, updatable=false)
private long companyId;
...
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(name="COM_ID")
private Company company;
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumns({
@JoinColumn(name="DEP_ID"),
@JoinColumn(name="COM_ID", insertable=false, updatable=false)
})
private Department department;
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumns({
@JoinColumn(name="MNG_ID"),
@JoinColumn(name="DEP_ID", insertable=false, updatable=false),
@JoinColumn(name="COM_ID", insertable=false, updatable=false)
})
private Employee manager;
@OneToOne(fetch=FetchType.LAZY)
@JoinColumns({
@JoinColumn(name="ADD_ID"),
@JoinColumn(name="DEP_ID", insertable=false, updatable=false),
@JoinColumn(name="COM_ID", insertable=false, updatable=false)
})
private Address address;
...
}
@Entity
@IdClass(EmployeeId.class)
public class Employee {
@Id
@Column(name="EMP_ID")
private long employeeId;
@Id
@Column(name="DEP_ID", insertable=false, updatable=false)
private long departmentId;
@Id
@Column(name="COM_ID", insertable=false, updatable=false)
private long companyId;
...
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(name="COM_ID")
private Company company;
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(name="DEP_ID")
@PrimaryKeyJoinColumn(name="COM_ID")
private Department department;
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(name="MNG_ID")
@PrimaryKeyJoinColumns({
@PrimaryKeyJoinColumn(name="DEP_ID")
@PrimaryKeyJoinColumn(name="COM_ID")
})
private Employee manager;
@OneToOne(fetch=FetchType.LAZY)
@JoinColumn(name="ADD_ID")
@PrimaryKeyJoinColumns({
@PrimaryKeyJoinColumn(name="DEP_ID")
@PrimaryKeyJoinColumn(name="COM_ID")
})
private Address address;
...
}
在某些資料模型中,你可能有一個透過連線表定義的OneToOne關係。例如,假設你已經有了EMPLOYEE和ADDRESS表,但沒有外部索引鍵,並且想要定義一個OneToOne關係,而無需更改現有表。為此,你可以定義一箇中間表,其中包含這兩個物件的primaryKey。這類似於ManyToMany關係,但如果在每個外部索引鍵上新增一個唯一約束,你就可以強制執行它是OneToOne(甚至OneToMany)。
JPA 使用 `<a rel="nofollow" class="external text" href="https://java.sun.com/javaee/5/docs/api/javax/persistence/JoinTable.html">@JoinTable</a>` 註解和 `<join-table>` XML 元素定義連線表。`JoinTable` 可用於 `ManyToMany` 或 `OneToMany` 對映,但 JPA 1.0 規範對它是否可用於 `OneToOne` 模糊不清。`JoinTable` 文件沒有說明它是否可以用於 `OneToOne`,但 `<one-to-one>` 的 XML 模式確實允許巢狀的 `<join-table>` 元素。某些 JPA 提供者可能支援此功能,而另一些則可能不支援。
如果您的 JPA 提供者不支援此功能,您可以透過定義 `OneToMany` 或 `ManyToMany` 關係並僅定義返回/設定集合中第一個元素的 get/set 方法來解決此問題。
EMPLOYEE(表)
| EMP_ID | FIRSTNAME | LASTNAME | SALARY |
| 1 | Bob | Way | 50000 |
| 2 | Sarah | Smith | 60000 |
EMP_ADD(表)
| EMP_ID | ADDR_ID |
| 1 | 6 |
| 2 | 7 |
ADDRESS(表)
| ADDRESS_ID | STREET | CITY | PROVINCE | COUNTRY | P_CODE |
| 6 | 17 Bank St | Ottawa | ON | Canada | K2H7Z5 |
| 7 | 22 Main St | Toronto | ON | Canada | L5H2D5 |
@OneToOne(fetch=FetchType.LAZY)
@JoinTable(
name="EMP_ADD",
joinColumns=
@JoinColumn(name="EMP_ID", referencedColumnName="EMP_ID"),
inverseJoinColumns=
@JoinColumn(name="ADDR_ID", referencedColumnName="ADDRESS_ID"))
private Address address;
...
@OneToMany
@JoinTable(
name="EMP_ADD"
joinColumns=
@JoinColumn(name="EMP_ID", referencedColumnName="EMP_ID"),
inverseJoinColumns=
@JoinColumn(name="ADDR_ID", referencedColumnName="ADDRESS_ID"))
private List<Address> addresses;
...
public Address getAddress() {
if (this.addresses.isEmpty()) {
return null;
}
return this.addresses.get(0);
}
public void setAddress(Address address) {
if (this.addresses.isEmpty()) {
this.addresses.add(address);
} else {
this.addresses.set(0, address);
}
}
...
