Technology » JPA, Hibernate, and Co. » JPA Composite Key Variants

JPA Composite Key Variants

Table of Contents

For entity classes that have composite primary keys, there are four basic JPA implementations that developers can choose from:

  1. JPA 1.0 @IdClass
  2. JPA 1.0 @EmbeddedId
  3. JPA 2.0 @IdClass
  4. JPA 2.0 @EmbeddedId

Example:

JPA Projects-Departments Example
JPA Projects-Departments Example

Simple logic: Projects references Departments and Departments references Companies, both using many-to-one cardinalities and identifying relationships. Companies has a single-column ID as primary key, whereas Departments and Projects both have composite primary keys. The example has two consequences:

  1. The Company class is constant for all variants (because it doesn't reference anything).
  2. The Project class references another class that has a composite primary key.

The latter consequence is especially interesting and the main purpose of this article. Pay attention to the ProjectId class, which only allows nestes referenciation of composite primary key class for JPA 2.0. Here's the constant Companies class:

Company.java:

@Entity
@Table(name = "Companies")
public class Company implements Serializable
{
    @Id
    @Column(name = "id")
    private Integer id;

    @Column(name = "name")
    private String name;

    ...
}

Note that, like above, I omitted all unnecessary stuff like imports, constructors, and methods in the forthcoming code examples.

JPA 1.0 @IdClass

The principle of JPA 1.0 @IdClass composite primary key implementations is the declaration of redundant fields for each one that is part of the primary key. Redundant fields are generally annotated with @Id and @Column and may optionally carry the readonly property (@Column(..., insertable=false, updatable=false)).

The types and variable names in the entity and composite primary key classes must match. Furthermore, JPA 1.0 does not allow the nesting of other (referenced) composite primary key classes. These must be decomposed into their parts.

Department.java:

@Entity
@Table(name = "Departments")
@IdClass(value = DepartmentId.class)
public class Department implements Serializable
{
    @Id
    @Column(name = "company_id", insertable = false, updatable = false)
    private Integer companyId;

    @Id
    @Column(name = "internal_code")
    private String internalCode;

    @Column(name = "name")
    private String name;

    @ManyToOne
    @JoinColumn(name = "company_id", referencedColumnName = "id")
    private Company company;

    ...
}

DepartmentId.java:

public class DepartmentId implements Serializable
{
    private Integer companyId;

    private String internalCode;
    
    ...
}

Project.java:

@Entity
@Table(name = "Projects")
@IdClass(value = ProjectId.class)
public class Project implements Serializable
{
    @Id
    @Column(name = "company_id", insertable = false, updatable = false)
    private Integer companyId;

    @Id
    @Column(name = "department_code", insertable = false, updatable = false)
    private String departmentCode;

    @Id
    @Column(name = "internal_code")
    private String internalCode;

    @Column(name = "name")
    private String name;

    @ManyToOne
    @JoinColumns(value = {@JoinColumn(name = "company_id", referencedColumnName = "company_id"), @JoinColumn(name = "department_code", referencedColumnName = "internal_code")})
    private Department department;
    
    ...
}

ProjectId.java:

public class ProjectId implements Serializable
{
    private Integer companyId;

    private String departmentCode;

    private String internalCode;

    ...
}

JPA 1.0 @EmbeddedId

This approach is a lot cleaner than the JPA 1.0 @IdClass mappings as it eliminates its redundant fields. The principle of JPA @EmbeddedId composite primary key mappings is to defer all primary key column mappings to a separate instance of the entity class - including all basic annotations (@Basic, @Column, @Temporal, @Binary, @Enum, ...).

This results in a separation of primary key column and non-primary key column mappings. The columns in the @Embeddable composite primary key class are automatically considered to have an @Id on them.

The only problem arising are columns that are part of the primary key, but are used in a relationship as well (identifying relationships). The solution here is that any relationship in the entity class keeps the @JoinColumn annotation(s), while each part also being a primary key is sufficiently mapped in the composite primary key class.

Department.java:

@Entity
@Table(name = "Departments")
public class Department implements Serializable
{
    @EmbeddedId
    private DepartmentId embeddedId;

    @Column(name = "name")
    private String name;

    @ManyToOne
    @JoinColumn(name = "company_id", referencedColumnName = "id")
    private Company company;

    ...
}

DepartmentId.java:

@Embeddable
public class DepartmentId implements Serializable
{
    @Column(name = "company_id", insertable = false, updatable = false)
    private Integer companyId;

    @Column(name = "internal_code")
    private String internalCode;

    ...
}

Project.java:

@Entity
@Table(name = "Projects")
public class Project implements Serializable
{
    @EmbeddedId
    private ProjectId embeddedId;

    @Column(name = "name")
    private String name;

    @ManyToOne
    @JoinColumns(value = {@JoinColumn(name = "company_id", referencedColumnName = "company_id"), @JoinColumn(name = "department_code", referencedColumnName = "internal_code")})
    private Department department;

    ...
}

ProjectId.java:

@Embeddable
public class ProjectId implements Serializable
{
    @Column(name = "company_id", insertable = false, updatable = false)
    private Integer companyId;

    @Column(name = "department_code", insertable = false, updatable = false)
    private String departmentCode;

    @Column(name = "internal_code")
    private String internalCode;

    ...
}

JPA 2.0 @IdClass

One of the biggest changes in JPA 2.0 specification are derived identifiers. It basically means that the @Id annotation is allowed to be put onto relationships, essentially making them identifying relationships. These can potentially be multi-column, which complicates composite primary key class mappings, but also gets rid of JPA 1.0 redundant fields. I personally think derived identifiers result in a cleaner-looking model without any redundancies.

Another change that had to be made in JPA 2.0 to allow nesting of composite primary key classes. This feature is quite logical, because of the @Id sitting on a potentially multi-column field representing a relationship to another entity.

As you can see from the code below, single-column relationships are mapped in the composite primary key with their simple types (because the target entity doesn't have a composite primary key class). For the multi-column relationship between Project and Department, there is one field dept in the entity class, which must get its counterpart in the composite primary key class (ProjectId) of type DepartmentId. The latter is an example of a nested composite primary key reference.

The types of this mapping can't match exactly as the fields in the composite primary key class must be of the type of the referenced entity's composite primary key class (if multi-column primary key) or they must be of a simple type (for single-column primary keys). Note the names of the mapped fields must match, no matter if they're single-column or multi-column.

The JPA 2.0 rule for composite primary key classes is: single-column references must be mapped with equaling types and variable names. Multi-column references must be mapped with the same variable names whose entity and composite primary key classes must match.

Department.java:

@Entity
@Table(name = "Departments")
@IdClass(value = DepartmentId.class)
public class Department implements Serializable
{
    @Id
    @Column(name = "internal_code")
    private String internalCode;

    @Column(name = "name")
    private String name;

    @Id
    @ManyToOne
    @JoinColumn(name = "company_id", referencedColumnName = "id")
    private Company company;

    ...
}

DepartmentId.java:

public class DepartmentId implements Serializable
{
    private Integer company;

    private String internalCode;

    ...
}

Project.java:

@Entity
@Table(name = "Projects")
@IdClass(value = ProjectId.class)
public class Project implements Serializable
{
    @Id
    @Column(name = "internal_code")
    private String internalCode;

    @Column(name = "name")
    private String name;

    @Id
    @ManyToOne
    @JoinColumns(value = {@JoinColumn(name = "company_id", referencedColumnName = "company_id"), @JoinColumn(name = "department_code", referencedColumnName = "internal_code")})
    private Department department;

    ...
}

ProjectId.java:

public class ProjectId implements Serializable
{
    private DepartmentId department;

    private String internalCode;

    ...
}

JPA 2.0 @EmbeddedId

Basically all that JPA 2.0 adds to the JPA 1.0 @EmbeddedId mappings is the @MapsId annotation. Because the embedded ID contains all primary key fields, it's not straightforward to tell which one maps to which field in the entity class. The @MapsId annotation explicitly maps the an entity class' field to on in a composite primary key class so that the strict rule of naming the fields the same can be relieved.

Note the JPA 1.0 @EmbeddedId mappings can successfully be handled by EclipseLink and Hibernate without the @MapsId annotation (for whatever reason). JPA providers seem to do a pretty good job of finding the right column combinations here.

Further note that as well as the JPA 2.0 @IdClass implementation the JPA 2.0 @EmbeddedId code may also nest composite primary key classes.

Department.java:

@Entity
@Table(name = "Departments")
public class Department implements Serializable
{
    @EmbeddedId
    private DepartmentId embeddedId;

    @Column(name = "name")
    private String name;

    @MapsId(value = "companyId")
    @ManyToOne
    @JoinColumn(name = "company_id", referencedColumnName = "id")
    private Company company;

    ...
}

DepartmentId.java:

@Embeddable
public class DepartmentId implements Serializable
{
    @Column(name = "company_id", insertable = false, updatable = false)
    private Integer companyId;

    @Column(name = "internal_code")
    private String internalCode;

    ...
}

Project.java:

@Entity
@Table(name = "Projects")
public class Project implements Serializable
{
    @EmbeddedId
    private ProjectId embeddedId;

    @Column(name = "name")
    private String name;

    @MapsId(value = "departmentId")
    @ManyToOne
    @JoinColumns(value = {@JoinColumn(name = "company_id", referencedColumnName = "company_id"), @JoinColumn(name = "department_code", referencedColumnName = "internal_code")})
    private Department department;

    ...
}

ProjectId.java:

@Embeddable
public class ProjectId implements Serializable
{
    @Embedded
    private DepartmentId departmentId;

    @Column(name = "internal_code")
    private String internalCode;

    ...
}

Summary

Having four basic variants of how to code composite primary keys in JPA makes it really hard to get the mappings right, so that JPA providers don't throw any strange exceptions. It's especially hard for newcomers to get through all the bad information on the Internet. I hope this article will help some people who have desparately been looking for a reference like this.

If you are attempting to map relationships to non-primary key columns please see my respective article here.