Mapping entity association

Often the first step to develop an application is to create the entities and define the relationships or associations between them. A relationship basically means that one entity holds a reference to another entity. For example, consider the following User and Passport entity.

As we can see, the User entity holds a reference of the Passport entity. So we can say, a relationship or association exists between the User and Passport entity. This relationship can be either unidirectional or bidirectional.

Unidirectional or bidirectional relationship

When only one out of the two entities contains a reference to the other, we say the association is unidirectional. For example, the relationship between User and Passport in the example above is unidirectional as only the User has a reference to Passport but the Passport has no reference to the User. So, we can access the Passport from User, but we can’t access User from Passport. In other words, we can access the relationship from one direction only.

But, if both the entities refer to each other, we say it is bi-directional. Let’s update our Passport entity as shown below.

Now we can say the relationship is bi-directional as both User and Passport entities have references to each other. So, we can access the Passport from User and User from Passport. In other words, you can access the relationship from both directions.

Direct and indirect reference

One entity can directly refer to another entity by using a field of that type as shown below.

One entity can also indirectly refer to another entity by using a collection of the other entity. In this case, we are not directly using a field of another entity type. Rather we are indirectly using another entity using a collection.

JPA association types

JPA and Hibernate support 4 types of association (relationship) and each of these association types is expressed through an annotation.

Relationship Type Annotation
one-to-one @OneToOne
one-to-many @OneToMany
many-to-one @ManyToOne
many-to-many @ManyToMany
One-to-one

In a one-to-one relationship, one entity refers to only one another entity. For example, one person has exactly one passport and each passport belongs to only one person. So the relationship between person and passport is one-to-one. In this type of relationship, in the database, each row in one table has only one related row in a second table.

This relationship has one owning side and one reference side. The entity that contains foreign key column – normally we make that as the owning side. In our example we will make the User as the owning side as it is holding the foreign key column. We can use @JoinColumn annotation to specify the foreign key column. Also, we have to use @OneToOne annotation to specify the association type.

@Entity
@Table(name = "PASSPORT", schema = "demo")
public class Passport {
    @Id
    @Column(name = "PASSPORT_ID")
    private String passportId;
    
    @Temporal(TemporalType.DATE)
    @Column(name = "EXPIRY_DATE")
    private Date expiryDate;
}
@Entity
@Table(name = "USER_DETAILS", schema = "demo")
public class User {
    @Id
    @Column(name = "USER_ID")
    private long userId;

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

    @OneToOne
    @JoinColumn(name = "PASSPORT_ID")
    private Passport passport;
}

As you can see we have defined a unidirectional relationship as only the User entity contains the reference of the Passport entity. If you try to save a User entity object after setting (calling setter method) a new passport object, you will get following error –

org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : com.demo.User.passport -> com.demo.Passport

The error message is pretty clear. You are trying to save a user object and it contains a passport object in the transient state. In other words, the user object is dependent on the passport object but during saving the User object, Hibernate detected that the Passport object was not saved. So, the passport object should be persisted first then the user object.

So, if you save the passport object first then the user object, it’ll work just fine.

entityManager.persist(newPassportObject);
entityManager.persist(user);

Consider you have a lot of associations along with your passport. Persisting them one by one by yourself might be painful. Now the question is – is there any easier solution available?

Cascading Operations

In many cases when an entity instance changes state, you may want to include associated entities to that state transition automatically. For example, when you persist a new User object, the state of the User object changes from transient to persistent. Along with that you may want to make the passport object persistent automatically so that it can be persisted along with the User object. The cascading settings allows you to achieve that.

There are different Cascading options available like –

  1. CascadeType.PERSIST
  2. CascadeType.REMOVE
  3. CascadeType.DETACH
  4. CascadeType.MERGE
  5. CascadeType.REFRESH
  6. CascadeType.ALL

CascadeType.PERSIST indicates that when an entity instance is persisted using the persist() method, all associated entities will also be persisted. Other cascading options will provide exactly the behavior as the name indicates. CascadeType.ALL is the special option that enables all the cascading options.

Coming back to our User and Passport entity, if we enable the cascading option PERSIST or ALL, we don’t have to persist Passport object and User object separately. Just persist the User object (owner) and the Passport object will be persisted automatically.

@Entity
@Table(name = "USER_DETAILS", schema = "demo")
public class User {
    @Id
    @Column(name = "USER_ID")
    private long userId;

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

    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "PASSPORT_ID")
    private Passport passport;
}

Let’s go one step further and make the relationship bi-directional. To do that, you have to add the User reference in the Passport entity and mark that reference as @OneToOne.

@Entity
@Table(name = "PASSPORT", schema = "demo")
public class Passport {
    @Id
    @Column(name = "PASSPORT_ID")
    private String passportId;
    
    @Temporal(TemporalType.DATE)
    @Column(name = "EXPIRY_DATE")
    private Date expiryDate;

    @OneToOne(mappedBy = "passport")
    private User user;
}

Let’s understand the mappedBy attribute. As we know, the annotation @JoinColumn is used for the owner entity that contains the foreign key column. Whereas the attribute mappedBy indicates that the entity is in the inverse or the referencing end of the bi-directional relationship. Also, as an argument we pass the name of the reference variable for this entity on the owner end.

One-to-many and many-to-one

In a one-to-many relationship, one entity will contain a collection of another entity. If the association between two entities is bidirectional, one side of the association is one-to-many and the other side of the association is many-to-one.

For one-to-many or many-to-one relationships, we will have two tables in the database. The table corresponding to the many sides will contain foreign key to refer to the primary key of the one side.

In a unidirectional one-to-Many relationship, the one side will keep the references of the many side and only one side will define the relationship using @OneToMany annotation.

But in a unidirectional many-to-one relationship, the many side will keep the reference of the one side and only the many side will define the relationship using @ManyToOne annotation.

In both cases, we can use @JoinColumn annotation to specify the foreign key column.

The relationship between State and City is one-to-many as one State can contain multiple cities. But one City belongs to only one state.

Now, let’s try to insert a state object that contains multiple city objects using one-to-many unidirectional mapping. Please add @JoinColumn in State entity.

@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "STATE_ID")
private Set<City> cities = new HashSet<City>();

Persist a State object using code similar to below.

State state = new State();
state.setStateName("Maharashtra");

City city1 = new City();
city1.setCityId(1L);
city1.setCityName("Mumbai");

City city2 = new City();
city2.setCityId(2L);
city2.setCityName("Pune");

state.getCities().add(city1);
state.getCities().add(city2);

entityManager.persist(state);

Hibernate will internally issue below queries –

insert into demo.STATE (STATE_NAME, STATE_ID) values (1, 'Maharashtra');
insert into demo.CITY (CITY_NAME, CITY_ID) values (1, 'Mumbai');
insert into demo.CITY (CITY_NAME, CITY_ID) values (2, 'Pune');

update demo.CITY set STATE_ID = 1 where CITY_ID = 1;
update demo.CITY set STATE_ID = 1 where CITY_ID = 2;

So you can see –

  1. Hibernate inserts the records first.
  2. Then Hibernate updates the Foreign Key column of the CITY table.

This will work. But will be a little slower as additional UPDATE statements are executed. In an ideal scenario, INSERT statements for the CITY table should include the Foreign Key value so that additional UPDATE is not required.

To achieve this, we need bidirectional @OneToMany mapping. Configuration of bidirectional one-to-many or many-to-one mapping is the same. In bidirectional mapping, we must use the mappedBy element of the @OneToMany annotations to specify the name of the reference variable on the many-to-one side. Also, specify the joinColumn at the many side.

You should implement the equals and hashCode methods in City. Because if you want to add or remove a city object from state, those methods will come into play.

Also, to keep both the entities in sync, whenever you add a city object to state, you should add the state reference to that city. The method to achieve this will look something like below –

public class State {
    private Set<City> cities = new HashSet<City>();

    public Set<City> getCities() {
        return cities;
    }

    public void addtCity(City city) {
        city.setState(this);
        getCities().add(city);
    }
}

You should add a city object to the state using this method.

The updated entities will look something like below –

@Entity
@Table(name = "STATE", schema = "DEMO")
public class State {
    @Id
    @Column(name = "STATE_ID")
    private long stateId;

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

    @OneToMany(mappedBy = "state", cascade = CascadeType.ALL)
    private Set<City> cities = new HashSet<City>();

    public Set<City> getCities() {
        return cities;
    }

    public void addtCity(City city) {
        city.setState(this);
        getCities().add(city);
    }

    // other getter setter
}
@Entity
@Table(name = "CITY", schema = "DEMO")
public class City {
    @Id
    @Column(name = "CITY_ID")
    private long cityId;
    
    @Column(name = "CITY_NAME")
    private String cityName;
    
    @ManyToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "STATE_ID")
    private State state;

    // getter, setter, equals, hashCode
}

If you now try to save a state object, hibernate will issue following queries –

insert into demo.STATE (STATE_NAME, STATE_ID) values (1, 'Maharashtra');
insert into demo.CITY (CITY_NAME, STATE_ID, CITY_ID) values (1, 1, 'Mumbai');
insert into demo.CITY (CITY_NAME, STATE_ID, CITY_ID) values (2, 1, 'Pune');

You see, additional update queries are no longer used as foreign key value is added along with the INSERT statement. So, the bidirectional @OneToMany association is the best way to map a one-to-many relationship.

many-to-many

In a many-to-many relationship, both sides can have multiple references of the other side. For example, consider facebook groups and facebook users. An user can have multiple groups and a group can contain multiple users. So, we can say, the relationship between the User and Group is many-to-many.

In java, User entity will contain a collection (preferably Set) of Groups and Group entity will contain a collection (preferably Set) of Users.

In the database, we need an additional table which contains the primary key pairs of both the entities. This table will map which user ids belong to a group id or which group ids belong to a user id.

In the example above, USER_GROUP is the additional table and it is indicating that Tom joined only the Hibernate Lions group whereas Jerry joined both the groups.

We will configure bi-directional many-to-many relationship as almost all the time you will use bi-directional relationship. 

To configure the association, the owner entity will provide all mapping information including additional table mapping. The other entity just specifies the mappedBy attribute of the @ManyToMany annotation. Also, both the entities should override equals and hashCode methods.

@Entity
@Table(name = "USER_DETAILS", schema = "demo")
public class User {
    @Id
    @Column(name = "USER_ID")
    private long userId;

    @Column(name = "USER_NAME")
    private String userName;
   
    @ManyToMany(cascade = CascadeType.ALL)
    @JoinTable(name = "USER_GROUP", schema = "DEMO",
        joinColumns = { @JoinColumn(name = "USER_ID") },
        inverseJoinColumns = { @JoinColumn(name = "GROUP_ID") })
    private Set<Group> groups = new HashSet<Group>();

    // getters, setters, equals, hashCode
}
@Entity
@Table(name = "FB_GROUP", schema = "DEMO")
public class Group {
    @Id
    @Column(name = "GROUP_ID")
    private long groupId;
    
    @Column(name = "GROUP_NAME")
    private String groupName;
    
    @ManyToMany(mappedBy = "groups")
    private Set<User> users = new HashSet<User>();
}

That’s all you have to do to map bidirectional many-to-many association between two entities. Also, to keep both the entities in sync, whenever you add a Group object to User, you should add the User reference to that Group. The method to achieve this will look something like below. You should use this method to add a group to a User.

@Entity
public class User {    
    private Set<Group> groups = new HashSet<Group>();
    
    public void addGroup(Group group) {
        group.getUsers().add(this);
        this.groups.add(group);
    }
}

That’s it for now. Hope you have enjoyed this tutorial. If you have any doubt, please ask in the comment section. I will try to answer that as soon as possible. Till then, bye bye.