跳到内容

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);
    }
  }
  ...
华夏公益教科书