Ternary Associations – Modelling Associations between 3 Entities


Take your skills to the next level!

The Persistence Hub is the place to be for every Java developer. It gives you access to all my premium video courses, monthly Java Persistence News, monthly coding problems, and regular expert sessions.


An association between 3 entities is called a ternary association. A typical example is an association between an employee, the project they are working on, and their role in that project. If the role is a complex object, you might decide to model this as 3 entity classes.

The association between them is the interesting part. You have 2 main options to model it on your entity classes. You either map the association table as an entity class or as a java.util.Map.

Table Model of a Ternary Association

Let’s first take a quick look at the table model. Each of the 3 entities and the association between them gets stored in its own database table.

I called the association table project_assignment. It follows the same concept as the association table of a many-to-many association. It contains the 3 foreign keys of the associated entities and uses all 3 of them as its derived primary key.

Mapping the Association as an Entity

The easiest approach is to map the association table as an entity. I map it to the ProjectAssignment class. 3 of it attributes are the many-to-one associations to the Employee, Project, and Role entities. The 4th one, the id attribute, is of type ProjectAssignmentId. This is an embeddable that models the 3 attributes of the primary key.

@Entity
public class ProjectAssignment {

    @EmbeddedId
    private ProjectAssignmentId id;

    @ManyToOne
    @MapsId("projectId")
    private Project project;

    @ManyToOne
    @MapsId("roleId")
    private Role role;

    @ManyToOne
    @MapsId("personId")
    private Person person;

    @Embeddable
    public static class ProjectAssignmentId implements Serializable {
        private Long projectId;
        private Long roleId;
        private Long personId;

        public ProjectAssignmentId() {}

        // getter and setter methods
		
        // equals and hashCode methods     
    }

    ...
}

The @MapsId annotations on the association attributes tell Hibernate to use the primary key value of the associated entities as a part of the primary key value of this entity.

The value of each @MapsId annotation references an attribute of the ProjectAssignmentId embeddable. This is the attribute, to which Hibernate will map the primary key value of the associated entity. That is a typical mapping of a derived primary key. I explain it in great detail in my Advanced Hibernate Online Training.

These are all required parts of the mapping. To make the associations easier to use, I recommend to model them bidirectionally. As I explain in my Guide to Association Mappings, you can easily do that by adding an attribute of type Set<ProjectAssignment> to each of them. You also need to annotate it with a @OneToMany annotation. Its mappedBy attribute references the name to the attribute on the ProjectAssignment entity that represents this association.

@Entity
public class Person {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;

    @Version
    private int version;

    private String firstName;
    
    private String lastName;

    @OneToMany(mappedBy = "person")
    Set<ProjectAssignment> assignments = new HashSet<>();

    ...
}

Mapping the Association as a Map

Hibernate can also map this association as a java.util.Map. As I showed in a previous article, that mapping is very flexible. In this example, I want to model the association on the Person entity. The Project entity becomes the map key, and the Role entity the value.

The implementation of such a mapping is straight forward. You need an attribute of type Map<Project, Role> and a few annotations:

  • The @OneToMany annotation defines the association.
  • The @JoinTable annotation specifies the name of the database table that represents this association.
  • The @MapKeyJoinColumn annotation tells Hibernate which column in the join table it shall use as the key of the Map.
@Entity
public class Person {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;

    @Version
    private int version;

    private String firstName;
    
    private String lastName;

    @OneToMany
    @JoinTable(name="project_assignment")
    @MapKeyJoinColumn(name = "project_id")
    private Map<Project, Role> projectRoles = new HashMap<>();

    ...
}

That’s all you need to do. You can now use the Map in your business code to add new project assignments or get the Role of a Person in a Project.

Person p = new Person();
p.setFirstName("Thorben");
p.setLastName("Janssen");
em.persist(p);

Project pr = new Project();
pr.setName("Hibernate Test Extension");
em.persist(pr);

Role r = new Role();
r.setName("Developer");
em.persist(r);

p.getProjectRoles().put(pr, r);

Conclusion

You can map a ternary association in multiple ways. The 2 most common ones map the association table to an entity class or a java.util.Map.

In most projects, I prefer to create an entity class that represents the association table. That mapping is closer to the table model, easier to understand, and more flexible.

But in some projects, your business logic always accesses the associated elements via their key. In these situations, Hibernate’s support to represent your association as a java.util.Map makes your job much easier.