Java Persistence/OneToOne

From Wikibooks, open books for an open world
Jump to navigation Jump to search

OneToOne[edit | edit source]

A OneToOne relationship in Java is where the source object has an attribute that references another target object and (if) that target object had the inverse relationship back to the source object it would also be a OneToOne relationship. All relationships in Java and JPA are unidirectional, in that if a source object references a target object there is no guarantee that the target object also has a relationship to the source object. This is different than a relational database, in which relationships are defined through foreign keys and querying such that the inverse query always exists.

JPA also defines a ManyToOne relationship, which is similar to a OneToOne relationship except that the inverse relationship (if it were defined) is a OneToMany relationship. The main difference between a OneToOne and a ManyToOne relationship in JPA is that a ManyToOne always contains a foreign key from the source object's table to the target object's table, where as a OneToOne relationship the foreign key may either be in the source object's table or the target object's table. If the foreign key is in the target object's table JPA requires that the relationship be bi-directional (must be defined in both objects), and the source object must use the mappedBy attribute to define the mapping.

In JPA a OneToOne relationship is defined through the @OneToOne annotation or the <one-to-one> element. A OneToOne relationship typically requires a @JoinColumn or @JoinColumns if a composite primary key is used.

Example of a OneToOne relationship database[edit | edit source]

EMPLOYEE (table)

EMP_ID FIRSTNAME LASTNAME SALARY ADDRESS_ID
1 Bob Way 50000 6
2 Sarah Smith 60000 7

ADDRESS (table)

ADDRESS_ID STREET CITY PROVINCE COUNTRY P_CODE
6 17 Bank St Ottawa ON Canada K2H7Z5
7 22 Main St Toronto ON Canada L5H2D5

Example of a OneToOne relationship annotations[edit | edit source]

@Entity
public class Employee {
  @Id
  @Column(name="EMP_ID")
  private long id;
  ...
  @OneToOne(fetch=FetchType.LAZY)
  @JoinColumn(name="ADDRESS_ID")
  private Address address;
  
}

Example of a OneToOne relationship XML[edit | edit source]

<entity name="Employee" class="org.acme.Employee" access="FIELD">
    <attributes>
        <id name="id">
            <column name="EMP_ID"/>
        </id>
        <one-to-one name="address" fetch="LAZY">
            <join-column name="ADDRESS_ID"/>
        </one-to-one>
    </attributes>
</entity>

Inverse Relationships, Target Foreign Keys and Mapped By[edit | edit source]

A typical object centric perspective of a OneToOne relationship has the data model mirror the object model, in that the source object has a pointer to the target object, so the database source table has a foreign key to the target table. This is not how things always work out in the database though, in fact many database developers would think having the foreign key in the target table to be logical, as this enforces the uniqueness of the OneToOne relationship. Personally I prefer the object perspective, however you will most likely encounter both.

To start considering a bi-directional OneToOne relationship, you do not require two foreign keys, one in each table, so a single foreign key in the owning side of the relationship is sufficient. In JPA the inverse OneToOne must use the mappedBy attribute (with some exceptions), this makes the JPA provider use the foreign key and mapping information in the source mapping to define the target mapping.

See also, Target Foreign Keys, Primary Key Join Columns, Cascade Primary Keys.

The following gives an example of what the inverse address relationship would look like.

Example of an inverse OneToOne relationship annotations[edit | edit source]

@Entity
public class Address {
  @Id
  @Column(name = "ADDRESS_ID")
  private long id;
  ...
  @OneToOne(fetch=FetchType.LAZY, mappedBy="address")
  private Employee owner;
  ...
}

Example of an inverse OneToOne relationship XML[edit | edit source]

<entity name="Address" class="org.acme.Address" access="FIELD">
    <attributes>
        <id name="id"/>
        <one-to-one name="owner" fetch="LAZY" mapped-by="address"/>
    </attributes>
</entity>

See Also[edit | edit source]

Common Problems[edit | edit source]

Foreign key is also part of the primary key.[edit | edit source]
See Primary Keys through OneToOne Relationships.
Foreign key is also mapped as a basic.[edit | edit source]
If you use the same field in two different mappings, you typically require to make one of them read-only using insertable, updatable = false.
See Target Foreign Keys, Primary Key Join Columns, Cascade Primary Keys.
Constraint error on insert.[edit | edit source]
This typically occurs because you have incorrectly mapped the foreign key in a OneToOne relationship.
See Target Foreign Keys, Primary Key Join Columns, Cascade Primary Keys.
It can also occur if your JPA provider does not support referential integrity, or does not resolve bi-directional constraints. In this case you may either need to remove the constraint, or use EntityManager flush() to ensure the order your objects are written in.
Foreign key value is null[edit | edit source]
Ensure you set the value of the object's OneToOne, if the OneToOne is part of a bi-directional OneToOne relationship, ensure you set OneToOne in both objects, JPA does not maintain bi-directional relationships for you.
Also check that you defined the JoinColumn correctly, ensure you did not set insertable, updatable = false or use a PrimaryKeyJoinColumn, or mappedBy.

Advanced[edit | edit source]

Target Foreign Keys, Primary Key Join Columns, Cascade Primary Keys[edit | edit source]

If a OneToOne relationship uses a target foreign key (the foreign key is in the target table, not the source table), then JPA requires that you define a OneToOne mapping in both directions, and that the target foreign key mapping use the mappedBy attribute. The reason for this, is the mapping in the source object only affects the row the JPA writes to the source table, if the foreign key is in the target table, JPA has no easy way to write this field.

There are other ways around this problem however. In JPA the JoinColumn defines an insertable and updatable attribute, these can be used to instruct the JPA provider that the foreign key is actually in the target object's table. With these enabled JPA will not write anything to the source table, most JPA providers will also infer that the foreign key constraint is in the target table to preserve referential integrity on insertion. JPA also defines the @PrimaryKeyJoinColumn that can be used to define the same thing. You still must map the foreign key in the target object in some fashion though, but could just use a Basic mapping to do this.

Some JPA providers may support an option for a unidirectional OneToOne mapping for target foreign keys.

Target foreign keys can be tricky to understand, so you might want to read this section twice. They can get even more complex though. If you have a data model that cascades primary keys then you can end up with a single OneToOne that has both a logical foreign key, but has some fields in it that are logically target foreign keys.

For example consider Company, Department, Employee. Company's id is COM_ID, Department's id is a composite primary key of COM_ID and DEPT_ID, and Employee's id is a composite primary key of COM_ID, DEP_ID, and EMP_ID. So for an Employee its relationship to company uses a normal ManyToOne with a foreign key, but its relationship to department uses a ManyToOne with a foreign key, but the COM_ID uses insertable, updatable = false or PrimaryKeyJoinColumn, because it is actually mapped through the company relationship. The Employee's relationship to its address then uses a normal foreign key for ADD_ID but a target foreign key for COM_ID, DEP_ID, and EMP_ID.

This may work in some JPA providers, others may require different configuration, or not support this type of data model.

Example of cascaded primary keys database[edit | edit source]

COMPANY(table)

COM_ID NAME
1 ACME
2 Wikimedia

DEPARTMENT(table)

COM_ID DEP_ID NAME
1 1 Billing
1 2 Research
2 1 Accounting
2 2 Research

EMPLOYEE (table)

COM_ID DEP_ID EMP_ID NAME MNG_ID ADD_ID
1 1 1 Bob Way null 1
1 1 2 Joe Smith 1 2
1 2 1 Sarah Way null 1
1 2 2 John Doe 1 2
2 1 1 Jane Doe null 1
2 2 1 Alice Smith null 1

ADDRESS(table)

COM_ID DEP_ID ADD_ID ADDRESS
1 1 1 17 Bank, Ottawa, ONT
1 1 2 22 Main, Ottawa, ONT
1 2 1 255 Main, Toronto, ONT
1 2 2 12 Main, Winnipeg, MAN
2 1 1 72 Riverside, Winnipeg, MAN
2 2 1 82 Riverside, Winnipeg, MAN

Example of cascaded primary keys and mixed OneToOne and ManyToOne mapping annotations[edit | edit source]

@Entity
@IdClass(EmployeeId.class)
public class Employee {
  @Id
  @Column(name="EMP_ID")
  private long employeeId;
  @Id
  @Column(name="DEP_ID", insertable=false, updatable=false)
  private long departmentId;
  @Id
  @Column(name="COM_ID", insertable=false, updatable=false)
  private long companyId;
  ...
  @ManyToOne(fetch=FetchType.LAZY)
  @JoinColumn(name="COM_ID")
  private Company company;
  @ManyToOne(fetch=FetchType.LAZY)
  @JoinColumns({
    @JoinColumn(name="DEP_ID"),
    @JoinColumn(name="COM_ID", insertable=false, updatable=false)
  })
  private Department department;
  @ManyToOne(fetch=FetchType.LAZY)
  @JoinColumns({
    @JoinColumn(name="MNG_ID"),
    @JoinColumn(name="DEP_ID", insertable=false, updatable=false),
    @JoinColumn(name="COM_ID", insertable=false, updatable=false)
  })
  private Employee manager;
  @OneToOne(fetch=FetchType.LAZY)
  @JoinColumns({
    @JoinColumn(name="ADD_ID"),
    @JoinColumn(name="DEP_ID", insertable=false, updatable=false),
    @JoinColumn(name="COM_ID", insertable=false, updatable=false)
  })
  private Address address;
  ...
}

Example of cascaded primary keys and mixed OneToOne and ManyToOne mapping annotations using PrimaryKeyJoinColumn[edit | edit source]

@Entity
@IdClass(EmployeeId.class)
public class Employee {
  @Id
  @Column(name="EMP_ID")
  private long employeeId;
  @Id
  @Column(name="DEP_ID", insertable=false, updatable=false)
  private long departmentId;
  @Id
  @Column(name="COM_ID", insertable=false, updatable=false)
  private long companyId;
  ...
  @ManyToOne(fetch=FetchType.LAZY)
  @JoinColumn(name="COM_ID")
  private Company company;
  @ManyToOne(fetch=FetchType.LAZY)
  @JoinColumn(name="DEP_ID")
  @PrimaryKeyJoinColumn(name="COM_ID")
  private Department department;
  @ManyToOne(fetch=FetchType.LAZY)
  @JoinColumn(name="MNG_ID")
  @PrimaryKeyJoinColumns({
    @PrimaryKeyJoinColumn(name="DEP_ID")
    @PrimaryKeyJoinColumn(name="COM_ID")
  })
  private Employee manager;
  @OneToOne(fetch=FetchType.LAZY)
  @JoinColumn(name="ADD_ID")
  @PrimaryKeyJoinColumns({
    @PrimaryKeyJoinColumn(name="DEP_ID")
    @PrimaryKeyJoinColumn(name="COM_ID")
  })
  private Address address;
  ...
}

Mapping a OneToOne Using a Join Table[edit | edit source]

In some data models, you may have a OneToOne relationship defined through a join table. For example consider you had existing EMPLOYEE and ADDRESS tables with no foreign key, and wanted to define a OneToOne relationship without changing the existing tables. To do this you could define an intermediate table that contained the primary key of both objects. This is similar to a ManyToMany relationship, but if you add a unique constraint to each foreign key you can enforce that it is OneToOne (or even OneToMany).

JPA defines a join table using the @JoinTable annotation and <join-table> XML element. A JoinTable can be used on a ManyToMany or OneToMany mappings, but the JPA 1.0 specification is vague whether it can be used on a OneToOne. The JoinTable documentation does not state that it can be used in a OneToOne, but the XML schema for <one-to-one> does allow a nested <join-table> element. Some JPA providers may support this, and others may not.

If your JPA provider does not support this, you can workaround the issue by instead defining a OneToMany or ManyToMany relationship and just define a get/set method that returns/sets the first element on the collection.

Example of a OneToOne using a JoinTable database[edit | edit source]

EMPLOYEE (table)

EMP_ID FIRSTNAME LASTNAME SALARY
1 Bob Way 50000
2 Sarah Smith 60000

EMP_ADD(table)

EMP_ID ADDR_ID
1 6
2 7

ADDRESS (table)

ADDRESS_ID STREET CITY PROVINCE COUNTRY P_CODE
6 17 Bank St Ottawa ON Canada K2H7Z5
7 22 Main St Toronto ON Canada L5H2D5

Example of a OneToOne using a JoinTable[edit | edit source]

  @OneToOne(fetch=FetchType.LAZY)
  @JoinTable(
      name="EMP_ADD",
      joinColumns=
        @JoinColumn(name="EMP_ID", referencedColumnName="EMP_ID"),
      inverseJoinColumns=
        @JoinColumn(name="ADDR_ID", referencedColumnName="ADDRESS_ID"))
  private Address address;
  ...

Example of simulating a OneToOne using a OneToMany JoinTable[edit | edit source]

  @OneToMany
  @JoinTable(
      name="EMP_ADD"
      joinColumns=
        @JoinColumn(name="EMP_ID", referencedColumnName="EMP_ID"),
      inverseJoinColumns=
        @JoinColumn(name="ADDR_ID", referencedColumnName="ADDRESS_ID"))
  private List<Address> addresses;
  ...
  public Address getAddress() {
    if (this.addresses.isEmpty()) {
      return null;
    }
    return this.addresses.get(0);
  }
  public void setAddress(Address address) {
    if (this.addresses.isEmpty()) {
      this.addresses.add(address);
    } else {
      this.addresses.set(0, address);
    }
  }
  ...