JPA with Hibernate – Relationship Mapping using Annotations

In this guide, I’ll show you how to create all of the common relationship mappings available using the JPA specification.  In my examples, Hibernate is used as the underlying implementation.  This isn’t a full tutorial, but instead, this guide can be used as a quick cheat sheet when required to setup future implementations so I keep it updated or add to it as required.  In this guide, we’ll be using only annotation based configurations. 

This is not a exhaustive list of all possible configurations of the four basic relationships possible but rather a explanation of the common setups.  In particular, within each type of relationship, the exact configuration used often depends on the specifics of the use case – in particular, who should be the relationship owner.  Concepts we will be covering(in summary) are relationship owner, unidirectional and bidirectional relationships and the 4 basic mapping types.

We’ll also present some use cases that, while possible, are not recommended due to various inconsistencies that may crop up.  These are discussed for completeness.

Note:

  • Getters and setters have been omitted for verbosity.
  • As primary key column names are used in JPA configurations, to prevent confusing stemming from the proliferation of “ID” or <TABLE_NAME>_ID column names being used everywhere, foreign key column names have been named “<TABLE NAME>_ID_FK” (instead of just <TABLE_NAME_ID> or “ID”) to distinguish themselves from the actual table primary key column.
  • Casecase configurations have been omitted to simplify the code.

Introduction

There are four basic relationships outlined in the JPA specification.

  1. One to one
  2. One to many
  3. Many to one
  4. Many to many

One to One

The one to one relationship is used when one entity is at most associated with one other entity.  Since it is one to one, the foreign key can linking the two entities can be on:

  • On either side of the relationship.
  • Using a third join table.

Thus, lets use an example Car and Engine.  A CAR has one ENGINE and a ENGINE is part of one CAR.

1. Foreign key car_id is on the Engine table.

@Entity
@Table(name = "car")
public class Car {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private long car_id;

    @OneToOne(mappedBy = "car")
    private Engine engine;
}


@Entity
@Table(name="engine")
public class Engine {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
	private long engine_id;

    @OneToOne
    @JoinColumn(name="car_id_fk")
	private Car car;
}

This is a bidirectional relationship with the Engine being the owner of the relationship.  

In the Car entity:

  • We are using the mappedBy attribute in the @OneToOne annotation.   We are telling Hibernate that the mapping between Car and Engine is found on the Engine(owner) side.

In the Engine (owner) entity:

  • In addition to using the @OneToOne annotation to denote the relationship, since the ENGINE table contains the foreign key, it must use the @JoinColumn to denote the mapping to the Car entity.   

In this example, it is possible to create a unidirectional relationship from Engine to Car, by omitting the Engine member in Car (and associated JPA annotations).  It is not possible to create a unidirectional relationship from Car to Engine (in any standard form).   I.e. The mappedBy attribute requires a bidirectional reference  (Engine to Car), a member of type Car in Engine to complete the mapping  and the @JoinColumn annotation cannot be used since the foreign key is on the ENGINE table – not CAR.

2. Foreign key engine_id is on the Car table.

@Entity
@Table(name = "car")
public class Car {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private long car_id;

    @OneToOne
    @JoinColumn(name="engine_id_fk")
    private Engine engine;
}


@Entity
@Table(name="engine")
public class Engine {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
	private long engine_id;

    // to create a bidirectional mapping
    @OneToOne(mappedBy = "engine")
    private Car car;	
}

The Car entity:

  1. Denotes the one-to-one relation via the @OneToOne annotation.
  2. Is the owner of the relationship
  3. Uses the @JoinColumn to denote the existence of the foreign column in the CAR table which will be used to perform the join. 

In the Engine entity, the mappedBy attribute is used to let Hibernate know the owner of the mapping is controlled by the engine property in Car.

3. A mapping table car_engine is used

First, we’ll decide on a owning side, Car in this case.

@Entity
@Table(name = "car")
public class Car {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private long car_id;

	@OneToOne
    @JoinTable(
            name = "car_engine", 
            joinColumns = { @JoinColumn(name = "car_id_fk") }, 
            inverseJoinColumns = { @JoinColumn(name = "engine_id_fk") }
        )    
	private Engine engine;
}


@Entity
@Table(name="engine")
public class Engine {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
	private long engine_id;

    @OneToOne(mappedBy="engine")
    private Car car;	
}

The Car entity:

  1. Denotes the one-to-one relation via the @OneToOne annotation.
  2. Is the owner of the relationship
  3. Uses the @JoinTable with a name attribute to specify the join table name in the database.
    1. The joinColumns attribute is used to identify the foreign key for this entity (Car)
    2. The inverseJoinColumn is used to identify the foreign key for the other entity (Engine).

In the Engine entity, the mappedBy attribute is used to let Hibernate know the owner of the mapping is controlled by the engine property in Car.  When creating a new Engine instance, it can be assigned to the Car via the setter method on the Car’s side. See example below:

Car honda = carDao.findById((long) 1).get(); ID=1
Engine e1 = engineDao.findById(1l).get();  // ID=1

honda.setEngine(e1)
carDao.save(honda);  		

It is possible to define both sides as owners.

@Entity
@Table(name = "car")
public class Car {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private long car_id;

	@OneToOne
    @JoinTable(
            name = "car_engine", 
            joinColumns = { @JoinColumn(name = "car_id_fk") }, 
            inverseJoinColumns = { @JoinColumn(name = "engine_id_fk") }
        )    
	private Engine engine;
}


@Entity
@Table(name="engine")
public class Engine {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
	private long engine_id;

    // to create a bidirectional mapping
	@OneToOne
    @JoinTable(
            name = "car_engine", 
            joinColumns = { @JoinColumn(name = "engine_id_fk") }, 
            inverseJoinColumns = { @JoinColumn(name = "car_id_fk") }
    )   
    private Car car;	
}

 

One to Many and Many to One

Many one-to-many relationships often form a parent-child relationship.  Unlike the one-to-one mapping (above), a one-to-many mapping cannot be mapped via a foreign key on the parent side.  It can only be mapped via:

  1. The foreign key on the child side (usually the case)
  2. The use of a mapping table. 

In this relationship, the owning side is usually the “many” side.   In this example, we’ll use two entities Car (as above) but introduce another entity called Wheel.  A car has 0 or more wheels and each wheel is associated to only one wheel. 

1. Foreign key CAR_ID is on the WHEEL table.

For a unidirectional Car to Wheel mapping, we need to specify the foreign key column on the WHEEL table using the @JoinColumn annotation.

@Entity
@Table(name = "car")
public class Car {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private long car_id;

	@OneToMany
	@JoinColumn(name = "car_id_fk")
	private Set wheels;
}


@Entity
@Table(name="wheel")
public class Wheel {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
	private long engine_id;
}

On the Car entity:

  1. The @OneToMany annotation indicates the relationship type.
  2. The @JoinColumn annotation is interesting.  When used with a @OneToMany, it indicates to Hibernate that this entity is the owner of the relationship and will set the foreign key on the other side.

For a bidirectional mapping, we need to choose the owning side.  In the standard use case, the many side, Wheel, is the owner.  See below:

@Entity
@Table(name = "car")
public class Car {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private long car_id;

	@OneToMany( mappedBy = "car")
	private Set wheels;
}


@Entity
@Table(name="wheel")
public class Wheel {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private long engine_id;

    @ManyToOne
    @JoinColumn(name="car_id_fk")
    private Car car;
}

On the Car entity:

  1. we can replace the annotations used in Car with the mappedBy attribute, to tell Hibernate that the relationship is tracked on the Wheel side (no need to create a third mapping table).  In this situation, we can remove the @JoinColumn annotation used above as the @ManyToOne and @JoinColumn provides Hibernate the necessary information to perform the mapping. 
  2. This can be omitted if you only want a unidirectional Wheel to Car mapping.

On the Wheel Entity:

  1. Using the @ManyToOne annotation on Wheel indicates that Wheel is the owner.  

For completeness, it is possible to have a bidirectional relationship where the Car is the owner.

@Entity
@Table(name = "car")
public class Car {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private long car_id;

	@OneToMany(cascade = {CascadeType.ALL})
	@JoinColumn(name = "cart_id_fk")
	private Set wheels;
}


@Entity
@Table(name="wheel")
public class Wheel {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private long engine_id;

    @ManyToOne
    @JoinColumn(name="car_id_fk")
    private Car car;
}

In the Car entity:

  1. The mapped by attribute in the @OneToMany annotation has been replaced by the @JoinColumn annotation.

In the Wheel entity:

  1. Nothing has changed from before.

While this setup does allow for the setting of the foreign key on Item from both sides of the relationship, unwanted inconsistencies can arise.  Consider the case where a new Wheel is added to a Honda’s collection but the Wheel’s own reference to Car is set to a Toyota.  What happens?

Car honda = carDao.findById(1l).get();
Car toyota = carDao.findById(2l).get();

Wheel wheel = new Wheel();
// set other properties of a wheel 
    
honda.getWheels().add(wheel);
wheel.setCar(toyota);
		
wheelDao.save(wheel);
carDao.save(honda);  		

 Well, unlike the example above where the Wheel was defined as the owner, in this new example, the Car side takes precedence.

2. A mapping table car_engine is used

This is the same table structure used in number 3 of the one-to-one mapping.  It can also be used for a one-to-many or even a many-to-many mapping.   As expected, we have to use the @JoinTable annotation specifying the join table and join columns.  These can be avoid if the table and column names are default names expected by Hibernate.

For the bidirectional setup, we first choose a owning side.  In our example, Wheel will be the owning side.

@Entity
@Table(name = "car")
public class Car {

   @Id
   @GeneratedValue(strategy=GenerationType.IDENTITY)
   private long car_id;

   @OneToMany (mappedBy="car")
   private Set wheels;
}


@Entity
@Table(name="wheel")
public class Wheel {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private long engine_id;

    @ManyToOne
    @JoinTable(
            name = "car_wheel", 
            joinColumns = { @JoinColumn(name = "wheel_id_fk") }, 
            inverseJoinColumns = { @JoinColumn(name = "car_id_fk") }
   )  
   private Car car;
}

In this example, the Car entity uses the mappedBy attribute to tell Hibernate that the owning side(Wheel) is tracked on the car member of the Wheel entity.

The Wheel entity:

  1. Denotes the many-to-one relation via the @ManyToOne annotation.  Remember, this is the inverse of a One-to-Many relationship.
  2. Is the owner of the relationship
  3. Uses the @JoinTable with a name attribute to specify the join table name in the database.
    1. The joinColumns attribute is used to identify the foreign key for this entity (Wheel)
    2. The inverseJoinColumn is used to identify the foreign key for the other entity (Car).

This is a bidirectional setup, but we can make it one unidirectional from Wheel to Car by removing the Wheel member in Car.  In this setup, because we are using the mappedBy attribute, we cannot make it unidirectional Car to Wheel by deleting the annotations in Wheel.  To see how to create a unidirectional Car to Wheel setup, see below:

A bidirectional setup with both sides being a owning is shown below but a unidirectional Car to Wheel or vice versa simply be removing their corresponding member in the entity and mapping annotations.  

@Entity
@Table(name = "car")
public class Car {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private long car_id;

	@OneToMany
    @JoinTable(
            name = "cart_box", 
            joinColumns = { @JoinColumn(name = "cart_id_fk") }, 
            inverseJoinColumns = { @JoinColumn(name = "box_id_fk") }
        )    
     
   private Set wheels;
}


@Entity
@Table(name="wheel")
public class Wheel {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private long engine_id;

    @ManyToOne
    @JoinTable(
            name = "car_wheel", 
            joinColumns = { @JoinColumn(name = "wheel_id_fk") }, 
            inverseJoinColumns = { @JoinColumn(name = "car_id_fk") }
   )  
   private Car car;
}

This is a bidirectional relationship with two owners defined.  It’s important to understand the inconsistency that may happen if a Wheel is both added to one Car collection of wheels but it’s own Car member is set to another Car.

Car honda = carDao.findById(1l).get();  // ID=1
Car toyota = carDao.findById(2l).get();  // ID=2

Wheel wheel1= wheelDao.findById((long) 1).get(); ID=1
    
honda.getWheels().add(wheel1);
wheel1.setCar(toyota);
		
wheelDao.save(wheel1);
carDao.save(honda);  		

The Honda is assigned a Wheel via it’s Wheel collection but the Wheel’s Car is set to a Toyota.  After saving both Car and Wheel, we get both mappings appearing in the database, which is wrong.

Many to Many

Many-to-many relationships are “almost” always implemented with a third join table.  In addition, when defining a many-to-many relationship, we need to define a owning side of the relationship.  Either entity can be a owning side, but not at the same time.

For this example, we’ll introduce a Person entity. A person(or car owner) can own many cars and a car can have many owners.  This defines our many-to-many relationship.

@Entity
@Table(name = "person")
public class Person{

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private long person_id;

    @ManyToMany
    @JoinTable(
            name = "person", 
            joinColumns = { @JoinColumn(name = "person_id_fk") }, 
            inverseJoinColumns = { @JoinColumn(name = "car_id_fk") }
   )   
   private Set<Car> cars;
}


@Entity
@Table(name="car")
public class Car {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private long car_id;

    @ManyToMany(mappedBy = "cars")
    private Set<Person> people;
}

In the Person (owner) entity:

  1. The @ManyToMany annotation denotes the type of relationship.
  2. The @JoinTable denotes the use of a join table and specifies the name.  
    1. The joinColumns attribute specifies the foreign key column to this entity (Person).
    2. The inverseJoinColumn attribute specifies the foreign key column to the inverse side of the relationship (I.e. the primary key on CAR).
  3. Even though we’re setting up a bidirectional mapping, we can create a unidirectional mapping from only Person to Car by omitting the collection of owners (Person) that a car has had. 
  4. We cannot create a unidirectional mapping from Car to Person simply by removing Car’s car member since Person is the owner of the relationship and Hibernate depends on it when figuring out the “mappedBy” instruction.  Instead, if we want a unidirectional Car to Person mapping, we need to make Person the relationship owner and “reverse” the annotations used above.  

In the Car entity, we used the @ManyToMany annotation to denote the relationship type.   The mappedBy attribute denotes that the people collection is mapped by the owner entity (Person).   In other words, adding a Car instance to a Person’s cars will create the mapping regardless of instances may be present in the Car’s person collection.  Consider the code snippet below:

Person joe = personDao.findById(1l).get();
Person peter = personDao.findById(2l).get();

Car honda = carDao.findById((long) 1).get();
    
joe.getCars().add(honda);
honda.getPeople().add(peter);
		
personDao.save(joe);
personDao.save(peter);
carDao.save(honda);  		

This code is attempting to assign a the Honda(a Car instance) to a person.  The code has a inconsistency in mapping as the Honda is being added to Joe’s list of cars but Peter is being added to his collection of Cars.  Because Hibernate will use the owner’s mapping to save the relationship and Joe will own a Honda and not Peter.

As mentioned above, this reverse mapping is to support the bidirectional mapping.  We can also perform a unidirectional mapping from Person to Car by removing the people member in the Car entity but we cannot remove the cars member in People as it is the owner of the relationship.

Because of the use of the join table, we can reverse the owner side.

Just to reiterate the mapping behavior, we can also have mapping a way which has two owners.  This method is not recommended:

@Entity
@Table(name = "person")
public class Person{

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private long person_id;

    @OneToMany
    @JoinTable(
            name = "person", 
            joinColumns = { @JoinColumn(name = "person_id_fk") }, 
            inverseJoinColumns = { @JoinColumn(name = "car_id_fk") }
   )   
   private Set<Car> cars;
}


@Entity
@Table(name="car")
public class Car {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private long car_id;

    @ManyToMany
    @JoinTable(
            name = "person", 
            joinColumns = { @JoinColumn(name = "car_id_fk") }, 
            inverseJoinColumns = { @JoinColumn(name = "person_id_fk") }
   )   
   private Set<Person> people;
}

It is important to know in this situation, since both relationships are ‘owners’, the following code:

Person joe = personDao.findById(1l).get();  // ID=1
Person peter = personDao.findById(2l).get();  // ID=2

Car honda = carDao.findById((long) 1).get(); ID=1
    
joe.getCars().add(honda);
honda.getPeople().add(peter);
		
personDao.save(joe);
personDao.save(peter);
carDao.save(honda);  		

The result in the PERSON_CAR table will be:

Both people will be associated to the Honda, which is probably not what we want. 

Conclusion

This is only a rough outline how the basic relationship types and their configurations.  It is important to know configurations will be dependent on the exact use case and other specifics.  For example, the addition of nullable=false on a mapping annotation may cause a particular configuration to no longer persist.  

 

 

Leave a Reply

Your email address will not be published. Required fields are marked *