跳到主要内容

一对一

本文通过介绍如何使用@org.babyfish.jimmer.sql.OneToOne注解可以声明一对一关联属性

一对一可支持双向关联,对于双向关联而言,其中一方必须主动方,另外一方为从动方。

  • 主动方(必须):真正的数据库和关联属性之间映射,实现单向一对一关联。

  • 从动方(可选):如果已经存在一个单向关联,可以为此配置从动方,作为主动方的镜像,形成双向关联。

我们假设存在用户Customer和地址Address两种实体类型,并在它们之间建立双向一对一关联。

信息

和JPA/Hibernate不同,主动方和从动方可以随意抉择,二者都可以用于保存关联。

本文例子抉择如下:

  • 主动方(必须):Customer.address

  • 从动方(可选):Address.customer

主动方

有两种方法可以实现一对一关联,基于外键和基于中间表。

1. 基于外键

Customer.java
@Entity
public interface Customer {

@OneToOne
Address address();

...省略其他代码...
}

这里并没有配合使用@JoinColumn明确指定外键列名,Jimmer会根据命名策略推导address属性对应的列名。

如果默认的命名策略未被用户覆盖,属性address的外键列名为ADDRESS_ID。所以,之前的代码和这里的代码等价。

因此,上面的代码和下面的代码等价

Customer.java
@Entity
public interface Customer {

@OneToOne
@JoinColumn(name = "ADDRESS_ID")
Address address();

...省略其他代码...
}

外键可真可假。伪外键在后续文档中讨论,这里假设外键是真实的,则数据库中对应的约束为

// 如果指向关联对象的外键是真的,建立外键约束
alter table CUSTOMER
add constraint FK_CUSTOMER__ADDRESS
/* highlight-next-line */
foreign key(ADDRESS_ID)
references ADDRESS(ID);

2. 基于中间表

Customer.java
@Entity
public interface Customer {

@Nullable
@OneToOne
@JoinTable
Address address();

...
}

这里,并没有为@JoinTable指定任何属性,Jimmer会根据命名策略推导address属性对应的列名。

如果默认的命名策略未被用户覆盖,推导出的中间表信息为:

  • 中间表表名: CUSTOMER_ADDRESS_MAPPING
  • 中间表指向当前实体的外键的列名: CUSTOMER_ID
  • 中间表指向关联实体的外键的列名: ADDRESS_ID

所以,之前的代码和这里的代码等价:

Customer.java
@Entity
public interface Customer {

@Nullable
@OneToOne
@JoinTable(
name = "CUSTOMER_ADDRESS_MAPPING",
joinColumnName = "CUSTOMER_ID",
inverseJoinColumnName = "ADDRESS_ID"
/* highlith-end */
)
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.customerCustomer.address的镜像。

Customer.java
@Entity
public interface Customer {

@OneToOne
@JoinColumn(name = "ADDRESS_ID")
Address address();

...省略其他代码...
}

mirror

Address.java
@Entity
public interface Address {

// `mappedBy`表示`Address.customer`
// 是`Customer.address`的镜像
@OneToOne(mappedBy = "address")
@Nullable
Customer customer();

...省略其他代码...
}
警告

注意

  • 一旦指定@OneToOnemappedBy属性,就不得使用比如@JoinColumn@JoinTable

  • 作为从动方的一对一关联属性必须可null