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提供者不支持引用完整性,或者无法解析双向约束,也会出现这种情况。在这种情况下,你可能需要删除约束,或者使用
EntityManager
flush()
来确保写入对象的顺序。
- 确保你设置了对象的
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);
}
}
...