跳到內容

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;
  
}

一對一關係 XML 示例

[編輯 | 編輯原始碼]
<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;
  ...
}

反向一對一關係 XML 示例

[編輯 | 編輯原始碼]
<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>

另請參閱

[編輯 | 編輯原始碼]

常見問題

[編輯 | 編輯原始碼]
外部索引鍵也是主鍵的一部分。
[編輯 | 編輯原始碼]
參見透過OneToOne關係實現主鍵
外部索引鍵也被對映為基本型別。
[編輯 | 編輯原始碼]
如果你在兩個不同的對映中使用同一個欄位,通常需要使用insertable, updatable = false將其中一個設定為只讀。
參見目標外部索引鍵、主鍵連線列、級聯主鍵
插入時約束錯誤。
[編輯 | 編輯原始碼]
這通常是由於你在OneToOne關係中錯誤地映射了外部索引鍵導致的。
參見目標外部索引鍵、主鍵連線列、級聯主鍵
如果你的JPA提供者不支援引用完整性,或者無法解析雙向約束,也會出現這種情況。在這種情況下,你可能需要刪除約束,或者使用EntityManager flush()來確保寫入物件的順序。
外部索引鍵值為空。
[編輯 | 編輯原始碼]
確保你設定了物件的OneToOne的值,如果OneToOne是雙向OneToOne關係的一部分,確保你在兩個物件中都設定了OneToOne,JPA不會為你維護雙向關係。
還要檢查你是否正確定義了JoinColumn,確保你沒有設定insertable, updatable = false,或者使用PrimaryKeyJoinColumnmappedBy

目標外部索引鍵、主鍵連線列、級聯主鍵

[編輯 | 編輯原始碼]

如果OneToOne關係使用目標外部索引鍵(外部索引鍵位於目標表,而不是源表),那麼JPA要求你在兩個方向都定義OneToOne對映,並且目標外部索引鍵對映使用mappedBy屬性。這樣做的原因是,源物件中的對映隻影響JPA寫入源表的行,如果外部索引鍵位於目標表,JPA就無法輕鬆寫入這個欄位。

但是,還有其他方法可以解決這個問題。在JPA中,JoinColumn定義了insertableupdatable屬性,這些屬性可以用來指示JPA提供者外部索引鍵實際上位於目標物件的表中。啟用這些屬性後,JPA不會向源表寫入任何內容,大多數JPA提供者也會推斷外部索引鍵約束位於目標表中,以在插入時保持引用完整性。JPA還定義了@PrimaryKeyJoinColumn,可用於定義相同的內容。但是,你仍然需要以某種方式對映目標物件中的外部索引鍵,但可以使用Basic對映來完成此操作。

一些JPA提供者可能支援針對目標外部索引鍵的單向OneToOne對映選項。

目標外部索引鍵可能難以理解,因此你可能需要閱讀本節兩次。但它們可能變得更加複雜。如果你的資料模型級聯主鍵,那麼你最終可能會得到一個只有一個邏輯外部索引鍵的OneToOne,但其中包含一些邏輯上是目標外部索引鍵的欄位。

例如,考慮CompanyDepartmentEmployeeCompany的id是COM_IDDepartment的id是COM_IDDEPT_ID的組合主鍵,Employee的id是COM_IDDEP_IDEMP_ID的組合主鍵。因此,對於一個Employee,它與company的關係使用一個普通的ManyToOne,帶有一個外部索引鍵,但它與department的關係使用一個ManyToOne,帶有一個外部索引鍵,但是COM_ID使用insertable, updatable = falsePrimaryKeyJoinColumn,因為它實際上是透過company關係對映的。Employee與其address的關係使用一個普通的ADD_ID外部索引鍵,但對COM_IDDEP_IDEMP_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

級聯主鍵和混合OneToOne和ManyToOne對映註釋示例

[編輯 | 編輯原始碼]
@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;
  ...
}

使用PrimaryKeyJoinColumn的級聯主鍵和混合OneToOne和ManyToOne對映註釋示例

[編輯 | 編輯原始碼]
@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

[編輯 | 編輯原始碼]

在某些資料模型中,你可能有一個透過連線表定義的OneToOne關係。例如,假設你已經有了EMPLOYEEADDRESS表,但沒有外部索引鍵,並且想要定義一個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 方法來解決此問題。

使用 JoinTable 資料庫的 OneToOne 示例

[編輯 | 編輯原始碼]

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

使用 JoinTable 的 OneToOne 示例

[編輯 | 編輯原始碼]
  @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 模擬 OneToOne 的示例

[編輯 | 編輯原始碼]
  @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);
    }
  }
  ...
華夏公益教科書