The best way to map a Composite Primary Key with JPA and Hibernate

Introduction

One of my readers asked me to help him map a Composite Primary Key using JPA and Hibernate. Because this is a recurrent question, I decided to write a blog post in which I describe this mapping is more detail.

Domain Model

A relational database composite key contains two or more columns which together for the primary key of a given table.

employee_phone_composite_key

In the diagram above, the employee table has a Composite Primary Key, which consists of two columns:

  • department_id
  • employee_number

Every Employee can also have a Phone, which uses the same composite key to reference its owning Employee.

To map this database table mapping, we need to isolate the compound key into an @Embeddable first:

@Embeddable
public class EmployeeId implements Serializable {

    @Column(name = "company_id")
    private Long companyId;

    @Column(name = "employee_number")
    private Long employeeNumber;

    public EmployeeId() {
    }

    public EmployeeId(Long companyId, Long employeeId) {
        this.companyId = companyId;
        this.employeeNumber = employeeId;
    }

    public Long getCompanyId() {
        return companyId;
    }

    public Long getEmployeeNumber() {
        return employeeNumber;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof EmployeeId)) return false;
        EmployeeId that = (EmployeeId) o;
        return Objects.equals(getCompanyId(), that.getCompanyId()) &&
                Objects.equals(getEmployeeNumber(), that.getEmployeeNumber());
    }

    @Override
    public int hashCode() {
        return Objects.hash(getCompanyId(), getEmployeeNumber());
    }
}

The Embeddable must be `Serializable` and we need to provide an implementation for `equals` and `hashCode`.

The Employee mapping looks as follows:

@Entity(name = "Employee")
@Table(name = "employee")
public class Employee {

    @EmbeddedId
    private EmployeeId id;

    private String name;

    public EmployeeId getId() {
        return id;
    }

    public void setId(EmployeeId id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

The @EmbeddedId is used to instruct Hibernate that the Employee entity uses a compound key.

The Phone mapping is rather straightforward as well:

@Entity(name = "Phone")
@Table(name = "phone")
public class Phone {

    @Id
    @Column(name = "`number`")
    private String number;

    @ManyToOne
    @JoinColumns({
        @JoinColumn(
            name = "company_id",
            referencedColumnName = "company_id"),
        @JoinColumn(
            name = "employee_number",
            referencedColumnName = "employee_number")
    })
    private Employee employee;

    public Employee getEmployee() {
        return employee;
    }

    public void setEmployee(Employee employee) {
        this.employee = employee;
    }

    public String getNumber() {
        return number;
    }

    public void setNumber(String number) {
        this.number = number;
    }
}

The Phone uses the number as an entity identifier since every phone number and the @ManyToOne mapping uses the two columns that are part of the compound key.

Testing time

To see how it works, consider the following persistence logic:

doInJPA(entityManager -> {
    Employee employee = new Employee();
    employee.setId(new EmployeeId(1L, 100L));
    employee.setName("Vlad Mihalcea");
    entityManager.persist(employee);
});

doInJPA(entityManager -> {
    Employee employee = entityManager.find(
        Employee.class, new EmployeeId(1L, 100L));
    Phone phone = new Phone();
    phone.setEmployee(employee);
    phone.setNumber("012-345-6789");
    entityManager.persist(phone);
});

doInJPA(entityManager -> {
    Phone phone = entityManager.find(Phone.class, "012-345-6789");
    assertNotNull(phone);
    assertEquals(new EmployeeId(1L, 100L), phone.getEmployee().getId());
});

Which generates the following SQL statements:

INSERT INTO employee (name, company_id, employee_number)
VALUES ('Vlad Mihalcea', 1, 100)

SELECT e.company_id AS company_1_0_0_ ,
       e.employee_number AS employee2_0_0_ ,
       e.name AS name3_0_0_
FROM   employee e
WHERE  e.company_id = 1
       AND e.employee_number = 100
        
INSERT INTO phone (company_id, employee_number, `number`) 
VALUES (1, 100, '012-345-6789')

SELECT p.number AS number1_1_0_ ,
       p.company_id AS company_2_1_0_ ,
       p.employee_number AS employee3_1_0_ ,
       e.company_id AS company_1_0_1_ ,
       e.employee_number AS employee2_0_1_ ,
       e.name AS name3_0_1_
FROM   phone p
LEFT OUTER JOIN employee e 
ON     p.company_id = e.company_id AND p.employee_number = e.employee_number
WHERE  p.number = '012-345-6789'

Mapping relationships using the Composite Key

We can even map relationships using the information provided within the Composite Key itself. In this particular example, the company_id references a Company entity which looks as follows:

@Entity(name = "Company")
@Table(name = "company")
public static class Company {

    @Id
    private Long id;

    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Company)) return false;
        Company company = (Company) o;
        return Objects.equals(getName(), company.getName());
    }

    @Override
    public int hashCode() {
        return Objects.hash(getName());
    }
}

We can have the Composite Key mapping referencing the Company entity withing the Employee entity:

@Entity(name = "Employee")
@Table(name = "employee")
public static class Employee {

    @EmbeddedId
    private EmployeeId id;

    private String name;

    @ManyToOne
    @JoinColumn(name = "company_id",insertable = false, updatable = false)
    private Company company;

    public EmployeeId getId() {
        return id;
    }

    public void setId(EmployeeId id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Notice that the @ManyToOne association instructs Hibernate to ignore inserts and updates issued on this mapping since the company_id is controlled by the @EmbeddedId.

Mapping a relationships inside @Embeddable

But that’s not all. We can even move the @ManyToOne inside the @Embeddable itself:

@Embeddable
public static class EmployeeId implements Serializable {

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

    @Column(name = "employee_number")
    private Long employeeNumber;

    public EmployeeId() {
    }

    public EmployeeId(Company company, Long employeeId) {
        this.company = company;
        this.employeeNumber = employeeId;
    }

    public Company getCompany() {
        return company;
    }

    public Long getEmployeeNumber() {
        return employeeNumber;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof EmployeeId)) return false;
        EmployeeId that = (EmployeeId) o;
        return Objects.equals(getCompany(), that.getCompany()) &&
                Objects.equals(getEmployeeNumber(), that.getEmployeeNumber());
    }

    @Override
    public int hashCode() {
        return Objects.hash(getCompany(), getEmployeeNumber());
    }
}

Now, the Employee mapping will no longer require the extra @ManyToOne association since it’s offered by the entity identifier:

@Entity(name = "Employee")
@Table(name = "employee")
public static class Employee {

    @EmbeddedId
    private EmployeeId id;

    private String name;

    public EmployeeId getId() {
        return id;
    }

    public void setId(EmployeeId id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

The persistence logic changes as follows:

Company company = doInJPA(entityManager -> {
    Company _company = new Company();
    _company.setId(1L);
    _company.setName("vladmihalcea.com");
    entityManager.persist(_company);
    return _company;
});

doInJPA(entityManager -> {
    Employee employee = new Employee();
    employee.setId(new EmployeeId(company, 100L));
    employee.setName("Vlad Mihalcea");
    entityManager.persist(employee);
});

doInJPA(entityManager -> {
    Employee employee = entityManager.find(
        Employee.class, 
        new EmployeeId(company, 100L)
    );
    Phone phone = new Phone();
    phone.setEmployee(employee);
    phone.setNumber("012-345-6789");
    entityManager.persist(phone);
});

doInJPA(entityManager -> {
    Phone phone = entityManager.find(Phone.class, "012-345-6789");
    assertNotNull(phone);
    assertEquals(new EmployeeId(company, 100L), phone.getEmployee().getId());
});

If you enjoyed this article, I bet you are going to love my book as well.

Conclusion

Knowing how to map a composite key is very important because many database schemas use this type of primary key. As demonstrated by this blog post, such a mapping is not complicated at all.

Code available on GitHub.

If you liked this article, you might want to subscribe to my newsletter too.

Advertisements

14 thoughts on “The best way to map a Composite Primary Key with JPA and Hibernate

    1. Easy peasy. Checkout the mapping on GitHub.

      Basically, it looks like this:

      @Entity(name = "Company")
      @Table(name = "company")
      public class Company {
      
          @Id
          private Long id;
      
          private String name;
      
          public Long getId() {
              return id;
          }
      
          public void setId(Long id) {
              this.id = id;
          }
      
          public String getName() {
              return name;
          }
      
          public void setName(String name) {
              this.name = name;
          }
      }
      
      @Entity(name = "Employee")
      @Table(name = "employee")
      public class Employee {
      
          @EmbeddedId
          private EmployeeId id;
      
          private String name;
      
          @ManyToOne
          @JoinColumn(name = "company_id",insertable = false, updatable = false)
          private Company company;
      
          public EmployeeId getId() {
              return id;
          }
      
          public void setId(EmployeeId id) {
              this.id = id;
          }
      
          public String getName() {
              return name;
          }
      
          public void setName(String name) {
              this.name = name;
          }
      }
      
      1. I think you could also use @ManyToOne and Company directly in the EmployeeId in Hibernate. Is this considered bad practice? The advantage is that companyId doesn’t need to be mapped (no insertable=false and so on is needed, also no @MapsId)

      2. I used 2 @ManyToOne mappings in the EmbeddedId for a link table in a many-to-many association, but I have never tried to mix a regular column (e.g. employee_number) and a @ManyToOne so far.

    1. Sure it can be done. This is the test that prooves it.

      This is the mapping:

      @Entity(name = "Company")
      @Table(name = "company")
      public static class Company {
      
          @Id
          private Long id;
      
          private String name;
      
          public Long getId() {
              return id;
          }
      
          public void setId(Long id) {
              this.id = id;
          }
      
          public String getName() {
              return name;
          }
      
          public void setName(String name) {
              this.name = name;
          }
      
          @Override
          public boolean equals(Object o) {
              if (this == o) return true;
              if (!(o instanceof Company)) return false;
              Company company = (Company) o;
              return Objects.equals(getName(), company.getName());
          }
      
          @Override
          public int hashCode() {
              return Objects.hash(getName());
          }
      }
      
      @Entity(name = "Employee")
      @Table(name = "employee")
      public static class Employee {
      
          @EmbeddedId
          private EmployeeId id;
      
          private String name;
      
          public EmployeeId getId() {
              return id;
          }
      
          public void setId(EmployeeId id) {
              this.id = id;
          }
      
          public String getName() {
              return name;
          }
      
          public void setName(String name) {
              this.name = name;
          }
      }
      
      @Entity(name = "Phone")
      @Table(name = "phone")
      public static class Phone {
      
          @Id
          @Column(name = "`number`")
          private String number;
      
          @ManyToOne
          @JoinColumns({
              @JoinColumn(
                  name = "company_id",
                  referencedColumnName = "company_id"),
              @JoinColumn(
                  name = "employee_number",
                  referencedColumnName = "employee_number")
          })
          private Employee employee;
      
          public Employee getEmployee() {
              return employee;
          }
      
          public void setEmployee(Employee employee) {
              this.employee = employee;
          }
      
          public String getNumber() {
              return number;
          }
      
          public void setNumber(String number) {
              this.number = number;
          }
      }
      
      @Embeddable
      public static class EmployeeId implements Serializable {
      
          @ManyToOne
          @JoinColumn(name = "company_id")
          private Company company;
      
          @Column(name = "employee_number")
          private Long employeeNumber;
      
          public EmployeeId() {
          }
      
          public EmployeeId(Company company, Long employeeId) {
              this.company = company;
              this.employeeNumber = employeeId;
          }
      
          public Company getCompany() {
              return company;
          }
      
          public Long getEmployeeNumber() {
              return employeeNumber;
          }
      
          @Override
          public boolean equals(Object o) {
              if (this == o) return true;
              if (!(o instanceof EmployeeId)) return false;
              EmployeeId that = (EmployeeId) o;
              return Objects.equals(getCompany(), that.getCompany()) &&
                      Objects.equals(getEmployeeNumber(), that.getEmployeeNumber());
          }
      
          @Override
          public int hashCode() {
              return Objects.hash(getCompany(), getEmployeeNumber());
          }
      }
      
  1. thanks for the nice article 🙂 it would be nice though if you could elaborate a bit on the Serializable part: EmployeeId implements Serializable, but Company doesn’t – so basically EmployeedId isn’t properly serializable… Is there an easy way to overcome this apart from making Employee serializable as well?

      1. Doesn’t Company need to be serialized, when an EmployeeId is serialized since EmployeeId has

        @ManyToOne
        @JoinColumn(name = “company_id”)
        private Company company;

        ?

      2. Why does Company need to be serialized? You only need that if you use EJB passivization, or you store the entity in the HttpSession. But, as long as you don’t have to marshall an entity, you don’t need to make it Serializable. An entity can map String or Integer attributes which are serializable, but that does not mean that the entity has to be serializable as wlel, right?

  2. Thank you very much for this blog, I have a question, what if we have to tables which has composite keys, for example:
    Table X:
    a_id(pk)(fk),
    b_id(pk)(fk)

    Table Y:
    b_id(pk)(fk),
    c_id(pk)(fk)

    and I have a many-to-many relationship between table X and table Y represented in a table Z.
    Table Z:
    a_id(pk)(fk),
    b_id(pk)(fk),
    c_id(pk)(fk)

    how can I map table z?

    Many thanks in advance.

  3. Thanks. In my case I have a classes A and B and the PK of A is a pair of B. I created a composite key class AsPk and put @ManyToOne in the two B instances in the PK class, with JoinColumn to map the PK of B them to two different columns of A. Everything works except that when I persist an A instance, I expect the two referenced B instances to be persisted automatically, through CascadeType.ALL, however it did not happen. Does Cascade work through the Composite Key classes or am I missing something here?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s