一对一
本文通过介绍如何使用@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
        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