Hibernate Association Mappings and JPA Entity Relationships

Hibernate association mappings link entities with @OneToOne, @ManyToOne, @OneToMany and @ManyToMany. Learn which side owns the foreign key, what mappedBy does, the default fetch types and which mapping to pick.

logo

A Hibernate association mapping tells Hibernate how two entities refer to each other and which database column stores that link. In Java, a gym member holds a reference to a membership card or a list of workouts, whereas in the database the same link is a foreign key column or a join table. Jakarta Persistence describes these links with four annotations, @OneToOne, @ManyToOne, @OneToMany and @ManyToMany, plus the mappedBy attribute that marks the side that does not hold the foreign key.

We map associations whenever one table refers to another, for example to load a member together with the membership card or to list the workouts of one member.

The following example puts the four annotations on a gym Member and its Workout entity.

// Member
@OneToOne(mappedBy = "member", cascade = CascadeType.ALL)
private MembershipCard card;

@OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Workout> workouts = new ArrayList<>();

@ManyToMany
@JoinTable(name = "member_class",
    joinColumns = @JoinColumn(name = "member_id"),
    inverseJoinColumns = @JoinColumn(name = "class_id"))
private Set<FitnessClass> classes = new HashSet<>();

// Workout
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
Member lokesh = new Member("Lokesh");
lokesh.setCard(new MembershipCard("Gold"));
lokesh.addWorkout(new Workout("Running", 30));
lokesh.joinClass(yoga);
em.persist(lokesh);

// insert into Member (name,id) values (?,?)
// insert into MembershipCard (member_id,plan,id) values (?,?,?)
// insert into Workout (activity,member_id,minutes,id) values (?,?,?,?)
// insert into member_class (member_id,class_id) values (?,?)

Notice that one persist() call writes all four rows, because cascade covers the card and the workouts, and Member owns the member_class join table.

Next, we look at the owning side of each association and build the gym example step by step.

1. Association Types and the Owning Side

Every association has a cardinality, which says how many entities can be on each side of the link. @ManyToOne and @OneToMany describe the same link seen from its two ends, so there are three kinds of relationships but four annotations.

AnnotationMeaningGym exampleWhere the link is storedDefault fetch
@OneToOneOne A has one BMember and MembershipCardForeign key in one of the two tablesEAGER
@ManyToOneMany A belong to one BWorkout to MemberForeign key in the A tableEAGER
@OneToManyOne A has many BMember to WorkoutForeign key in the B table (with mappedBy)LAZY
@ManyToManyMany A relate to many BMember and FitnessClassSeparate join tableLAZY

The fetch type decides when Hibernate reads the associated rows, because EAGER loads them together with the entity, whereas LAZY loads them on first access. We set fetch = FetchType.LAZY on @ManyToOne and on the owning side of @OneToOne, because eager loading runs extra queries that we often do not need, as FAQ 3.5 shows.

1.1. Unidirectional and Bidirectional Associations

The direction of an association says which entity has a field that points to the other one.

  • A unidirectional association has a field in one entity only. Workout has a member field, and Member knows nothing about workouts.
  • A bidirectional association has a field in both entities. Workout.member and Member.workouts describe the same foreign key.
// Unidirectional: only the Workout side
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;

// Bidirectional: add the other side to Member
@OneToMany(mappedBy = "member")
private List<Workout> workouts = new ArrayList<>();

Adding the workouts field changes only the Java model, so the Member table gets no new column, and the member_id column in the Workout table stores the link in both cases.

1.2. Owning Side, Inverse Side and mappedBy

A bidirectional association has two fields but only one foreign key, so Hibernate needs to know which field to read when it writes that key. The owning side is the field Hibernate uses to write the foreign key or the join table row, whereas the inverse side carries mappedBy and Hibernate ignores it for writes. The value of mappedBy is the name of the field on the owning side.

Jakarta Persistence fixes the owning side for some annotations.

AssociationOwning sideInverse side
One-to-oneThe entity whose table has the foreign key (@JoinColumn)The other entity, with mappedBy
One-to-many / many-to-oneAlways the @ManyToOne sideThe @OneToMany side, with mappedBy
Many-to-manyEither entity; we pick it and put @JoinTable thereThe other entity, with mappedBy

@ManyToOne has no mappedBy attribute at all, because the “many” table always holds the foreign key.

1.3. Cascade and orphanRemoval on Associations

Two more attributes on the association annotations control what happens to the associated entities.

  • The cascade attribute passes an operation such as persist() or remove() from the parent to the associated entities.
  • The attribute orphanRemoval = true deletes a child row when the child is removed from the parent’s collection or reference.

Both attributes are independent of mappedBy. The owning side decides which field writes the foreign key, whereas cascade decides which operations Hibernate repeats on the associated entities.

1.4. Which Mapping Do I Need?

We pick the annotation by asking two questions about the data. The first is how many B one A can have, and the second is whether one B can belong to many A.

Decision grid: each A has one B and each B belongs to one A gives @OneToOne with the key in MembershipCard.member_id; each A has one B shared by many A gives @ManyToOne with the key in Workout.member_id; each A has many B that belong to one A gives @OneToMany plus @ManyToOne with the key in Workout.member_id; many to many gives @ManyToMany with the member_class join table
Two questions about the data pick the annotation. Using the association from both entities only adds a mappedBy field.

Each case of the decision grid has its own detailed guide.

What we needMappingForeign keyDetailed guide
A member has exactly one card@OneToOne + @JoinColumn on the card, mappedBy on the memberMembershipCard.member_idOne-to-one mapping
A workout belongs to one member@ManyToOne on WorkoutWorkout.member_idOne-to-many mapping
A member lists their workoutsAdd @OneToMany(mappedBy = “member”) on MemberWorkout.member_idOne-to-many mapping
Members join many classes, classes have many members@ManyToMany + @JoinTable on one side, mappedBy on the otherJoin table member_classMany-to-many mapping

2. Association Mapping Example

The following example is a gym app that runs on Hibernate 7.4, Java 25 and an in-memory H2 database. The complete project on GitHub maps all three relationships, prints the SQL for each step and includes 15 JUnit tests (mvn -q compile exec:java, mvn test).

2.1. The Gym Domain Model

A member has one membership card (“Gold”), logs many workouts (“Running”, 30 minutes) and joins fitness classes (“Yoga”, “Spinning”) that other members join too.

Entity diagram of the gym domain: MembershipCard has the member_id foreign key to Member (one-to-one), Workout has the member_id foreign key to Member (many-to-one), and the member_class join table holds member_id and class_id for the many-to-many between Member and FitnessClass
The Member table has no foreign key. The card and workout tables hold member_id, and the many-to-many is stored in the member_class join table.

Hibernate created these tables from the mappings.

create table Member (id bigint not null, name varchar(255), primary key (id))
create table MembershipCard (id bigint not null, member_id bigint unique, plan varchar(255), primary key (id))
create table Workout (minutes integer not null, id bigint not null, member_id bigint, activity varchar(255), primary key (id))
create table FitnessClass (id bigint not null, name varchar(255), primary key (id))
create table member_class (class_id bigint not null, member_id bigint not null, primary key (class_id, member_id))

2.2. One-to-One Mapping

The card table holds member_id, so MembershipCard is the owning side of the one-to-one mapping. Member points back with mappedBy = “member”, and a helper method sets both fields.

@OneToOne                         // fetch defaults to EAGER
@JoinColumn(name = "member_id")
private Member member;
@OneToOne(mappedBy = "member", cascade = CascadeType.ALL)
private MembershipCard card;

public void setCard(MembershipCard card) {
  this.card = card;
  card.setMember(this);
}

Hibernate adds a unique constraint to member_id, so the database itself allows only one card per member.

2.3. One-to-Many and Many-to-One Mapping

In the one-to-many mapping, Workout holds the member_id foreign key with @ManyToOne, and Member lists the workouts with @OneToMany(mappedBy = “member”).

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
@OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Workout> workouts = new ArrayList<>();

public void addWorkout(Workout workout) {
  workouts.add(workout);
  workout.setMember(this);
}

public void removeWorkout(Workout workout) {
  workouts.remove(workout);
  workout.setMember(null);
}

A workout cannot exist without its member, so the collection uses orphanRemoval. Removing one workout deletes its row.

member.removeWorkout(rowing);    // delete from Workout where id=?

2.4. Many-to-Many Mapping

A class has many members and a member joins many classes, so neither table can hold a single foreign key. We make Member the owning side and describe the join table there.

@ManyToMany
@JoinTable(name = "member_class",
    joinColumns = @JoinColumn(name = "member_id"),
    inverseJoinColumns = @JoinColumn(name = "class_id"))
private Set<FitnessClass> classes = new HashSet<>();

public void joinClass(FitnessClass fitnessClass) {
  classes.add(fitnessClass);
  fitnessClass.getMembers().add(this);
}
@ManyToMany(mappedBy = "classes")
private Set<Member> members = new HashSet<>();

Joining and leaving a class only inserts or deletes a row in the join table, and the FitnessClass rows stay unchanged.

member.joinClass(spinning);    // insert into member_class (member_id,class_id) values (?,?)
member.leaveClass(yoga);       // delete from member_class where member_id=? and class_id=?

The classes field has no cascade, because a fitness class is shared by many members, so deleting a member must remove only the member’s rows in member_class, never the classes. A many-to-many link with extra columns needs a different mapping.

2.5. Loading a Member and Its Associations

The default fetch types decide what one em.find() reads. We load a member and ask the PersistenceUnitUtil which fields are already loaded.

Member member = em.find(Member.class, memberId);
// select m1_0.id,c1_0.id,c1_0.plan,m1_0.name from Member m1_0
//   left join MembershipCard c1_0 on m1_0.id=c1_0.member_id where m1_0.id=?

boolean cardLoaded = util.isLoaded(member, "card");           // true  (@OneToOne, EAGER)
boolean workoutsLoaded = util.isLoaded(member, "workouts");   // false (@OneToMany, LAZY)
boolean classesLoaded = util.isLoaded(member, "classes");     // false (@ManyToMany, LAZY)

int workoutCount = member.getWorkouts().size();               // 2
// select w1_0.member_id,w1_0.id,w1_0.activity,w1_0.minutes from Workout w1_0 where w1_0.member_id=?

Hibernate reads the lazy collections with their own select on first access, inside the same transaction.

2.6. Deleting a Member

The call em.remove(member) follows the cascades, so Hibernate deletes the card and the workouts with the member and removes the join table rows, while the classes stay.

delete from member_class where member_id=?
delete from MembershipCard where id=?
delete from Workout where id=?          -- one per workout
delete from Member where id=?

3. Association Mapping FAQs

3.1. What Is the Difference Between @JoinColumn and mappedBy?

The two go on opposite sides of the same association. @JoinColumn names the foreign key column on the owning side, and mappedBy tells Hibernate that the other side already maps it.

@JoinColumnmappedBy
Placed onOwning side (Workout.member)Inverse side (Member.workouts)
Creates a columnYes, member_idNo
Used for writesYesNo
ValueColumn nameField name on the owning side

3.2. Why Is the Foreign Key Null After Saving the Child?

Because we updated only the inverse side. Hibernate ignores the mappedBy field when it writes, so it saves the new workout without its member.

member.getWorkouts().add(new Workout("Cycling", 45));
em.persist(member.getWorkouts().getLast());
// insert into Workout (activity,member_id,minutes,id) values (?,?,?,?)
// member_id = NULL

yoga.getMembers().add(member);   // no SQL, no member_class row

Always set the owning side, and keep both sides in sync with helper methods such as addWorkout() and joinClass(). Setting workout.setMember(member) alone is enough for the database, because after a reload, member.getWorkouts() contains the new workout.

3.3. Why Does @OneToMany Create an Extra Join Table?

Because a @OneToMany without mappedBy and without @JoinColumn is unidirectional, and its default mapping is a join table. Our Trainer entity maps its classes this way.

@OneToMany
private List<FitnessClass> classes = new ArrayList<>();
create table Trainer_FitnessClass (Trainer_id bigint not null, classes_id bigint not null unique)
insert into Trainer_FitnessClass (Trainer_id,classes_id) values (?,?)

The unique constraint on classes_id keeps it one-to-many, but every link costs one more table and one more insert. To store the key in the child table, we map it from the child with @ManyToOne, as Workout does, and add @OneToMany(mappedBy = …) on the parent only if we need the collection.

3.4. Should Every Association Be Bidirectional?

No. We add the inverse side only when the code reads the association from that entity, because each bidirectional association needs helper methods to keep both sides in sync, and a forgotten one leads to the null foreign key from FAQ 3.2.

The owning side alone can list the workouts of a member with a query.

List<String> activities = em.createQuery(
        "select w.activity from Workout w where w.member.id = :id order by w.activity", String.class)
    .setParameter("id", memberId)
    .getResultList();             // activities = [Rowing, Running]
// select w1_0.activity from Workout w1_0 where w1_0.member_id=? order by w1_0.activity

Member.workouts is worth adding when we load a member and change its workouts through it, as with orphanRemoval in section 2.3.

3.5. Should I Use FetchType.EAGER?

No, in most cases. Hibernate loads an EAGER association every time it loads the entity, including through a query. MembershipCard.member keeps the default EAGER fetch, so a query for three cards runs four statements.

List<MembershipCard> cards = em.createQuery("from MembershipCard", MembershipCard.class).getResultList();
// select mc1_0.id,mc1_0.member_id,mc1_0.plan from MembershipCard mc1_0
// select m1_0.id,c1_0.id,c1_0.plan,m1_0.name from Member m1_0 ... where m1_0.id=?   (card 1)
// select m1_0.id,c1_0.id,c1_0.plan,m1_0.name from Member m1_0 ... where m1_0.id=?   (card 2)
// select m1_0.id,c1_0.id,c1_0.plan,m1_0.name from Member m1_0 ... where m1_0.id=?   (card 3)

The extra statements are the N+1 select problem, which means one query for the list plus one per row. We map @ManyToOne and @OneToOne with fetch = FetchType.LAZY and load what a use case needs with join fetch, which reads the cards and their members in one statement.

List<MembershipCard> cards = em.createQuery("from MembershipCard c join fetch c.member", MembershipCard.class).getResultList();
// select mc1_0.id,m1_0.id,m1_0.name,mc1_0.plan from MembershipCard mc1_0
//   join Member m1_0 on m1_0.id=mc1_0.member_id

4. Conclusion

Hibernate association mappings cover three relationships with four annotations, and they follow one rule. The owning side writes the foreign key, and the inverse side only points to it with mappedBy. We let the “many” side own one-to-many links, put @JoinTable on one side of a many-to-many, make single-valued associations LAZY and keep both sides in sync with helper methods.

5. References

Happy Learning !!

Source Code on Github

Leave a Comment

  1. Thanks, Lokesh sir, You cleared my confusion about owner and owned entity with respect to mappedBy attribute. I googled, read the book and visited jboss official site and tired my eyes but did not get satisfactory answer. These sites just say formal jargon and typical stereotype definition that left me bewildered. Thanks man you explain it. You gave one example consist of EmployeEntity and AccountEntity and said that EmployeEntity is owner and AccountEntity is managedBy(mappedBy). You made the point clear further by explicitly show as follows

    Let’s say association is managed by EmployeeEntity then the annotations in both entity will look like below.
    //Inside EmployeeEntity.java
    @OneToOne
    AccountEntity account;

    //Inside AccountEntity.java
    @OneToOne (mappedBy = “account”)
    EmployeeEntity employee;

    Thanks again. Keep it up. We need your help

  2. Lokesh,

    First thanks to your great work. I am new to hibernate, but I can understand your article quite easily. I understand that both are bidirectional and scenario 1 has circular dependency which is bad, and second scenario does the right thing.

    However I am a little confused by your statement at the following lines,

    “Also observe the insert queries above. Both tables have foreign key association with column names employee_ID and account_ID respectively. It’s example of circular dependency in data and can easily bring down your application any time.”

    Are these two foreign keys specified in database schema during table creations or they are just the artifact of circular dependency?

  3. In second scenario, account table is not having any column to connect with employee ?
    How is it getting account.getEmployee() ?

    Secondly if one way mapping is able to get and insert data in both table as shown above ?
    Why do we need bi-directional ?

    In my opinion as i tried the same with JPA, we can not do account.getEmployee() in unidirectional
    I am getting a null pointer.

    • Second scenario is not example of unidirectional mapping. It’s also bidirectional. It differs from scenario 1 only from point – who owns the association. In first case, both sides can manage the relationship; in second case, EmployeeEntity manages the association.

  4. The example code for scenario 2) above, is not in sync with the explanation given.
    Currrent:—————————————————–
    //Inside EmployeeEntity.java
    @OneToOne
    AccountEntity account;

    //Inside AccountEntity.java
    @OneToOne (mappedBy = “account”)
    EmployeeEntity employee;

    Corrected as per explanation:——————————
    //Inside EmployeeEntity.java
    @OneToOne(mappedBy = “employee”)
    AccountEntity account;

    //Inside AccountEntity.java
    @OneToOne
    EmployeeEntity employee;

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.