One to One relations in JPA 2.0 22 Mar 2012

Recently I came across the need to map a one 2 one relationship from the object model to the database using JPA 2.0. The case was pretty simple as the database was nicely organized but it raised the question: what if? What if I have to deal to a legacy system or a database administrator that has to follow strict company rules. Other reasons such as security or performances may interfere with simple designs. Hence, I had a look to the diverse ways to map a one 2 one relationship. I probably forgot several cases so please do not hesitate to discuss them in the comments.

A one to one relationship consider that the objects involved in the relation are highly dependent. In Object Orientation, this corresponds to the an aggregation or a composition. In a relational model, the data can be either:

  1. in the same table,
  2. split over two (or more) tables (one per object) and linked by a foreign key, or
  3. split over two (or more) tables and linked by a join table.

The rest of the articles these describes different situations. Please note that for the sake of the explanation, I explicitly map all the fields even if most of the time the default mapping policy would work.

Data in the same class: Embedded class

This is especially useful for legacy code where the database design is a bit to flat for your taste. The following figure presents the concept of an embedded class. The class Address is embedded in the class Student. The idea is that Address is an entity per se, it exists only in the context of the class Student.

  One 2 One relations as an embedded class
  
One 2 One relations as an embedded class

To declare a embedded class, the class itself must be annotated with @Embeddable and its reference must be annotated with @Embedded.

@Embeddable
public class Address implements Serializable {

	...
    @Column(name = "NUMBER")
    private String number;
    
    @Column(name = "STREET")
    private String street;
    ...
} 

@Embedded and @Basic cannot be used together. Therefore, if required, lazy fetching must be declared field by field in the embedded class. Remember that outside of a JEE container, the actual behavior of lazy loading on @Basic and @OneToOne depend on the actual implementation. Eclipse link, for instance, does not perform lazy loading by default on @Basic, @OneToOne, and @ManyToOne mappings. For more detail, please refer to the Eclipse-link specification.

@Entity
@Table(name = "STUDENTS")
public class Student implements Serializable {
    ...

    @Id
    @Column(name = "ID")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long mId;

    ...
    
    @Embedded
    private Address mAddress;

    ...
}

The following SQL statement shows the code generated by the previous mapping.

SELECT  ID, FIRST_NAME, PHONE_NUMBER, BIRTH_DATE, LAST_NAME, NUMBER, 
        CITY, STREET, POSTAL_CODE 
  FROM  STUDENTS

Data in different classes: Secondary Tables

In the second scenario, the OO model is composed on only one class but the relational model is split over several tables.

The foreign key is in the secondary table

  Data is in different tables with the foreign key in the secondary table
  
Data is in different tables with the foreign key in the secondary table

In this case, the concepts of secondary tables is very useful. A secondary table is basically a table that hosts important data that are one to one related to the data of the primary table. Unlike the first case, a join is required and thus a key mapping is required. In the following mapping, the secondary table PICTURES is mapped using its STUDENT_ID field to the ID field of the main table.

@SecondaryTable(name = "PICTURES", 
    pkJoinColumns = @PrimaryKeyJoinColumn(name = "STUDENT_ID", 
        referencedColumnName = "ID"))
public class Student implements Serializable {

    ...
    @Id
    @Column(name = "ID")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long mId;

    /** A picture of the student. */
    @Lob
    @Basic(optional = true, fetch = FetchType.EAGER)
    @Column(table = "PICTURES", name = "PICTURE", nullable = true)
    private byte[] mPicture;
    ...

The following SQL statement shows the code generated by the previous mapping. It joins the two tables according to the @PrimaryKeyJoinColumn annotation.

SELECT  t0.ID, t1.STUDENT_ID, t1.PICTURE, t0.FIRST_NAME, t0.PHONE_NUMBER, t0.BIRTH_DATE,
        t0.LAST_NAME, t0.NUMBER, t0.CITY, t0.STREET, t0.POSTAL_CODE 
  FROM  STUDENTS t0, PICTURES t1 
 WHERE  (t1.STUDENT_ID = t0.ID)
  

The foreign key is in the host table

Similarly to the previous case, the data is split over several tables. The difference lies in the foreign key position. Here the foreign key is hosted in the primary table.

  Data is in different tables with the foreign key in the primary/host table
  
Data is in different tables with the foreign key in the primary/host table

From a mapping point of view, it is very similar to the previous case. Indeed only the key column names have to be changed to reflect the organization.

@SecondaryTable(name = "PICTURES", 
    pkJoinColumns = @PrimaryKeyJoinColumn(name = "PICTURE_ID", 
        referencedColumnName = "PICTURE_ID"))
public class Student implements Serializable {

The following SQL statement shows the code generated by the previous mapping. Again, the data are joined according to the content of the @PrimaryKeyJoinColumn annotation.

SELECT  t0.ID, t0.PICTURE_ID, t1.PICTURE_ID, t1.PICTURE, t0.FIRST_NAME, t0.PHONE_NUMBER, t0.BIRTH_DATE, 
        t0.LAST_NAME,t0.NUMBER, t0.CITY, t0.STREET, t0.POSTAL_CODE 
  FROM  STUDENTS t0, PICTURES t1 
 WHERE  (t1.PICTURE_ID = t0.PICTURE_ID)

First and second class JPA citizens

In the previous examples, the table PICTURES does not have a business existence in itself. It is a secondary table because its data only have meaning in relation to the data of the primary table. Sometimes, we may want to treat the second objects as first class JPA citizens and thus we must put the @Entity annotation on it. In this case, we have to use the @OneToOne annotation for the mapping. Unlike the previous mappings, @OneToOne enable bidirectional mapping.

In the last example, the tables STUDENTS and BADGES have a one to one relationship modeled with a foreign in the BADGES table to the STUDENTS table. In this case, the owning side is the Badge entity has it contains the foreign key.

As mentioned before, the class Badge is now an entity has it has a business existence without Student. The interesting part is the @JoinColumn annotation that specifies the local column that is the foreign key (STUDENT_ID) as well as to which column of the foreign table its corresponds (ID).

@Entity
@Table(name = "BADGES")
public class Badge implements Serializable {
	...
	
    @Id
    @Column(name = "ID")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long mId;

    @Column(name = "SECURITY_LEVEL")
    private Long mSecurityLevel;

    @OneToOne
    @JoinColumn(name = "STUDENT_ID", referencedColumnName = "ID")
    private Student mStudent;
    
    ...

As we want the other side to be aware of the relation (bidirectional), it is required to add the mappedBy attribute to the @OneToOne annotation. This attribute references the (Java) property in the entity that is the owner of the relationship.

@Entity
...
public class Student implements Serializable {

	...
    @Id
    @Column(name = "ID")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long mId;


    /** The Student's badge. */
    @OneToOne(mappedBy = "mStudent")
    private Badge mBadge;
	...
}

Using the previous mapping, JPA 2.0 produces the following SQL statements to load the Student object.

SELECT t0.ID, t1.STUDENT_ID, t1.PICTURE, t0.FIRST_NAME, t0.PHONE_NUMBER,  
       t0.BIRTH_DATE, t0.LAST_NAME, t0.NUMBER, t0.CITY, t0.STREET, 
       t0.POSTAL_CODE 
  FROM STUDENTS t0, PICTURES t1 
 WHERE (t1.STUDENT_ID = t0.ID)

SELECT ID, SECURITY_LEVEL, STUDENT_ID 
  FROM BADGES 
 WHERE (STUDENT_ID = ?)

Conclusion

JPA 2.0 offers many way to represent one to one relationships. This flexibility allows to handle many different scenarios that may happen when integrating legacy databases. The examples used in this blog are to be found in the JEE-6-Demo project on Google code hosting.