One To One
This chapter introduces how to use the @org.babyfish.jimmer.sql.OneToOne
annotation to declare one-to-one association properties.
One-to-one supports bidirectional association. For bidirectional associations, one side must be the owning side and the other side must be the inverse side:
-
Owning side (required): The actual mapping between the database and association properties to implement unidirectional one-to-one association.
-
Inverse side (optional): If a unidirectional association already exists, the inverse side can be configured as a mirror of the owning side to form a bidirectional association.
Let's assume there are two entity types Customer and Address, and establish a bidirectional one-to-one association between them.
Unlike JPA/Hibernate, the owning side and inverse side can be chosen arbitrarily. Both can be used to persist associations.
The example in this article chooses:
-
Owning side (required):
Customer.address
-
Inverse side (optional):
Address.customer
Owning Side
There are two ways to implement a one-to-one association: based on foreign key and based on join table.
1. Based on Foreign Key
- Java
- Kotlin
@Entity
public interface Customer {
@OneToOne
Address address();
...Omit other code...
}
@Entity
interface Customer {
@OneToOne
val address: Address
...Omit other code...
}
Here @JoinColumn
is not used together to explicitly specify the foreign key column name. Jimmer will deduce the column name corresponding to the address
property based on the naming strategy.
If the default naming strategy is not overridden by the user, the foreign key column name of the address
property is ADDRESS_ID
. So the previous code is equivalent to:
Therefore, the above code is equivalent to the following:
- Java
- Kotlin
@Entity
public interface Customer {
@OneToOne
@JoinColumn(name = "ADDRESS_ID")
Address address();
...Omit other code...
}
@Entity
interface Customer {
@OneToOne
@JoinColumn(name = "ADDRESS_ID")
val address: Address
...Omit other code...
}
The foreign key can be real or fake. Fake foreign keys are discussed in subsequent documents. Here we assume the foreign key is real, so the corresponding constraint in the database is:
// If foreign key pointing to associated entity is real, create foreign key constraint
alter table CUSTOMER
add constraint FK_CUSTOMER__ADDRESS
/* highlight-next-line */
foreign key(ADDRESS_ID)
references ADDRESS(ID);
2. Based on Join Table
- Java
- Kotlin
@Entity
public interface Customer {
@Nullable
@OneToOne
@JoinTable
Address address();
...
}
@Entity
interface Customer {
@OneToOne
@JoinTable
val address: Address?
...
}
Here, no properties are specified for @JoinTable
. Jimmer will deduce the column names corresponding to the address
property based on the naming strategy.
If the default naming strategy is not overridden by the user, the deduced join table information is:
- Join table name: CUSTOMER_ADDRESS_MAPPING
- Join table foreign key column pointing to current entity: CUSTOMER_ID
- Join table foreign key column pointing to associated entity: ADDRESS_ID
So the previous code is equivalent to:
- 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?
...
}
The join table CUSTOMER_ADDRESS_MAPPING
is defined as follows:
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);
// If foreign key pointing to current entity is real,
// create foreign key constraint
alter table CUSTOMER_ADDRESS_MAPPING
add constraint FK_CUSTOMER_ADDRESS_MAPPING__CUSTOMER
foreign key(CUSTOMER_ID)
references CUSTOMER(ID);
// If foreign key pointing to associated entity is real,
// create foreign key constraint
alter table CUSTOMER_ADDRESS_MAPPING
add constraint FK_CUSTOMER_ADDRESS_MAPPING__ADDRESS
foreign key(ADDRESS_ID)
references ADDRESS(ID);
// These two constraints are very important.
// Otherwise this join table expresses a many-to-many association
// rather than a one-to-one association
alter table CUSTOMER_ADDRESS_MAPPING
add constraint UQ_CUSTOMER_ADDRESS_MAPPING__CUSTOMER_ID
unique(CUSTOMER_ID);
alter table CUSTOMER_ADDRESS_MAPPING
add constraint UQ_CUSTOMER_ADDRESS_MAPPING__ADDRESS_ID
unique(ADDRESS_ID);
-
The join table has only two foreign keys, both non-null. The join table maintains the association by inserting and deleting data, never storing null data itself.
-
The join table has no corresponding ORM entity and does not need an independent primary key. The two foreign keys combined can serve as the primary key.
-
By default, the join table represents a many-to-many association. To degrade it to a one-to-one association, a unique constraint must be specified for each foreign key of the join table.
-
Unless for compatibility with existing database design, one-to-one associations are recommended to directly use foreign keys instead of join tables.
-
Once a one-to-one association is mapped using a join table, the Jimmer association property must be nullable, because the database cannot guarantee that any entity must have corresponding data in the join table.
Inverse Side
The configuration of the inverse side is very simple: just specify that Address.customer
is the mirror of Customer.address
.
In the following code:
-
Left: The owning side discussed in the previous section
-
Right: The inverse side association
Address.customer
introduced in this section, which is the mirror ofCustomer.address
Here, @OneToOne(mappedBy = "address")
indicates that the current property Address.customer
is the mirror of Customer.address
.
- Java
- Kotlin
@Entity
public interface Customer {
@OneToOne
@JoinColumn(name = "ADDRESS_ID")
Address address();
...Omit other code...
}
@Entity
interface Customer {
@OneToOne
@JoinColumn(name = "ADDRESS_ID")
val address: Address
...Omit other code...
}
- Java
- Kotlin
@Entity
public interface Address {
// `mappedBy` indicates `Address.customer`
// is the mirror of `Customer.address`
@OneToOne(mappedBy = "address")
@Nullable
Customer customer();
...Omit other code...
}
@Entity
interface Address {
// `mappedBy` indicates `Address.customer`
// is the mirror of `Customer.address`
@OneToOne(mappedBy = "address")
val customer: Customer?
...Omit other code...
}
-
Once the
mappedBy
property of@OneToOne
is specified, annotations like@JoinColumn
and@JoinTable
must not be used. -
The one-to-one association property as the inverse side must be nullable.