Skip to main content

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.

info

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

Customer.java
@Entity
public interface Customer {

@OneToOne
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:

Customer.java
@Entity
public interface Customer {

@OneToOne
@JoinColumn(name = "ADDRESS_ID")
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

Customer.java
@Entity
public interface Customer {

@Nullable
@OneToOne
@JoinTable
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:

Customer.java
@Entity
public interface Customer {

@Nullable
@OneToOne
@JoinTable(
name = "CUSTOMER_ADDRESS_MAPPING",
joinColumnName = "CUSTOMER_ID",
inverseJoinColumnName = "ADDRESS_ID"
)
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.

caution
  • 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 of Customer.address

Here, @OneToOne(mappedBy = "address") indicates that the current property Address.customer is the mirror of Customer.address.

Customer.java
@Entity
public interface Customer {

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

...Omit other code...
}

mirror

Address.java
@Entity
public interface Address {

// `mappedBy` indicates `Address.customer`
// is the mirror of `Customer.address`
@OneToOne(mappedBy = "address")
@Nullable
Customer customer();

...Omit other code...
}
caution
  • 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.