一对一
本文通过介绍如何使用@org.babyfish.jimmer.sql.OneToOne
注解可以声明一对一关联属性
一对一可支持双向关联,对于双向关联而言,其中一方必须主动方,另外一方为从动方。
-
主动方(必须):真正的数据库和关联属性之间映射,实现单向一对一关联。
-
从动方(可选):如果已经存在一个单向关联,可以为此配置从动方,作为主动方的镜像,形成双向关联。
我们假设存在用户Customer和地址Address两种实体类型,并在它们之间建立双向一对一关联。
和JPA/Hibernate不同,主动方和从动方可以随意抉择,二者都可以用于保存关联。
本文例子抉择如下:
-
主动方(必须):
Customer.address
-
从动方(可选):
Address.customer
主动方
有两种方法可以实现一对一关联,基于外键和基于中间表。
1. 基于外键
- Java
- Kotlin
@Entity
public interface Customer {
@OneToOne
Address address();
...省略其他代码...
}
@Entity
interface Customer {
@OneToOne
val address: Address
...省略其他代码...
}
这里并没有配合使用@JoinColumn
明确指定外键列名,Jimmer会根据命名策略推导address
属性对应的列名。
如果默认的命名策略未被用户覆盖,属性address
的外键列名为ADDRESS_ID
。所以,之前的代码和这里的代码等价。
因此,上面的代码和下面的代码等价
- Java
- Kotlin
@Entity
public interface Customer {
@OneToOne
@JoinColumn(name = "ADDRESS_ID")
Address address();
...省略其他代码...
}
@Entity
interface Customer {
@OneToOne
@JoinColumn(name = "ADDRESS_ID")
val address: Address
...省略其他代码...
}
外键可真可假。伪外键在后续文档中讨论,这里假设外键是真实的,则数据库中对应的约束为
// 如果指向关联对象的外键是真的,建立外键约束
alter table CUSTOMER
add constraint FK_CUSTOMER__ADDRESS
/* highlight-next-line */
foreign key(ADDRESS_ID)
references ADDRESS(ID);
2. 基于中间表
- Java
- Kotlin
@Entity
public interface Customer {
@Nullable
@OneToOne
@JoinTable
Address address();
...
}
@Entity
interface Customer {
@OneToOne
@JoinTable
val address: Address?
...
}
这里,并没有 为@JoinTable
指定任何属性,Jimmer会根据命名策略推导address
属性对应的列名。
如果默认的命名策略未被用户覆盖,推导出的中间表信息为:
- 中间表表名: CUSTOMER_ADDRESS_MAPPING
- 中间表指向当前实体的外键的列名: CUSTOMER_ID
- 中间表指向关联实体的外键的列名: ADDRESS_ID
所以,之前的代码和这里的代码等价:
- Java
- Kotlin
@Entity
public interface Customer {
@Nullable
@OneToOne
@JoinTable(
name = "CUSTOMER_ADDRESS_MAPPING",
joinColumnName = "CUSTOMER_ID",
inverseJoinColumnName = "ADDRESS_ID"
)
Address address();
...
}
@Entity
interface Customer {
@OneToOne
@JoinTable(
name = "CUSTOMER_ADDRESS_MAPPING",
joinColumnName = "CUSTOMER_ID",
inverseJoinColumnName = "ADDRESS_ID"
)
val address: Address?
...
}
中间表CUSTOMER_ADDRESS_MAPPING
定义如下
create table CUSTOMER_ADDRESS_MAPPING(
CUSTOMER_ID bigint not null,
ADDRESS_ID bigint not null
);
alter table ADDRESS_MAPPING
add constraint PK_ADDRESS_MAPPING
primary(CUSTOMER_ID, ADDRESS_ID);
// 如果指向当前对象的外键是真的,建立外键约束
alter table CUSTOMER_ADDRESS_MAPPING
add constraint FK_CUSTOMER_ADDRESS_MAPPING__CUSTOMER
foreign key(CUSTOMER_ID)
references CUSTOMER(ID);
// 如果指向关联对象的外键是真的,建立外键约束
alter table CUSTOMER_ADDRESS_MAPPING
add constraint FK_CUSTOMER_ADDRESS_MAPPING__ADDRESS
foreign key(ADDRESS_ID)
references ADDRESS(ID);
// 这两个约束非常重要。
// 否则这张中间表表达的是多对多关联,而非一对一关联
// 为中间表 CUSTOMER_ID 字段设置唯一约束
alter table CUSTOMER_ADDRESS_MAPPING
add constraint UQ_CUSTOMER_ADDRESS_MAPPING__CUSTOMER_ID
unique(CUSTOMER_ID);
// 为中间表 ADDRESS_ID 字段设置唯一约束
alter table CUSTOMER_ADDRESS_MAPPING
add constraint UQ_CUSTOMER_ADDRESS_MAPPING__ADDRESS_ID
unique(ADDRESS_ID);
-
中间表的只有两个外键,而且都非null。中间表靠插入数据和删除数据维护关联,本身从不存储null数据
-
中间表没有对应的ORM实体,无需独立主键,两个外键联合作为主键即可
-
默认情况下,中间表表示多对多关联。要让其退化为一对一关联,必须为中间表的每一个外键都指定唯一约束
注意
-
除非为了兼容已有数据库设计,一对一关联都建议直接使用外键,而非中间表
-
一旦使用中间表映射一对一关联,Jimmer关联属性必须可null,因为数据库无法保证任何实体在中间表中一定有对应数据
从动方
从动方的配置非常简单,指定Address.customer
属性是Customer.address
属性的镜像即可。
在下面的代码中
-
左侧:上一节中讨论过的主动方
-
右侧:本节要介绍从动方关联
Address.customer
,它是Customer.address
的镜像
这里,@OneToOne(mappedBy = "address")
,指当前属性Address.customer
是Customer.address
的镜像。
- Java
- Kotlin
@Entity
public interface Customer {
@OneToOne
@JoinColumn(name = "ADDRESS_ID")
Address address();
...省略其他代码...
}
@Entity
interface Customer {
@OneToOne
@JoinColumn(name = "ADDRESS_ID")
val address: Address
...省略其他代码...
}
- Java
- Kotlin
@Entity
public interface Address {
// `mappedBy`表示`Address.customer`
// 是`Customer.address`的镜像
@OneToOne(mappedBy = "address")
@Nullable
Customer customer();
...省略其他代码...
}
@Entity
interface Address {
// `mappedBy`表示`Address.customer`
// 是`Customer.address`的镜像
@OneToOne(mappedBy = "address")
val customer: Customer?
...省略其他代码...
}
注意
-
一旦指定
@OneToOne
的mappedBy
属性,就不得使用比如@JoinColumn
和@JoinTable
-
作为从动方的一对一关联属性必须可null