Hibernate Relationships
Hibernate Many-to-Many Example Using Annotations
Map a many-to-many relationship between questions and answers using Jakarta Persistence annotations and a join table.
Many-to-Many Association
A many-to-many relationship means that one question can reference many answers and one answer can be associated with many questions. Hibernate represents the relationship with three tables:
The join table contains one row for every question-answer association. A direct many-to-many mapping is suitable when the association has no additional attributes. If the relationship needs fields such as vote count, creation time, or moderation status, model the join table as its own entity.
1. Add Maven Dependencies
This example uses Hibernate 6, Jakarta Persistence, and an in-memory H2 database.
<dependency> <groupId>org.hibernate.orm</groupId> <artifactId>hibernate-core</artifactId> <version>6.6.9.Final</version> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>2.3.232</version> <scope>runtime</scope> </dependency>
Use the JDBC driver and compatible dialect for MySQL, PostgreSQL, Oracle, or another database. Hibernate 5 projects may use the older javax.persistence namespace, but should not mix it with Jakarta imports.
2. Create the Question Entity
The question entity is the owning side because it declares @JoinTable. A Set is often safer than a list for a direct many-to-many relationship because it prevents duplicate associations when equality is defined correctly.
package com.example;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToMany;
import java.util.HashSet;
import java.util.Set;
@Entity
public class Question {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String questionText;
@ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE })
@JoinTable(
name = "question_answer",
joinColumns = @JoinColumn(name = "question_id"),
inverseJoinColumns = @JoinColumn(name = "answer_id")
)
private Set<Answer> answers = new HashSet<>();
protected Question() { }
public Question(String questionText) {
this.questionText = questionText;
}
public void addAnswer(Answer answer) {
answers.add(answer);
answer.getQuestions().add(this);
}
public Set<Answer> getAnswers() { return answers; }
public String getQuestionText() { return questionText; }
}CascadeType.ALL is intentionally not used here because an answer may be shared by other questions. Cascade only the lifecycle operations that match the application's ownership rules.
3. Create the Answer Entity
The inverse side uses mappedBy and does not declare another join table. This prevents Hibernate from trying to manage the same association twice.
package com.example;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToMany;
import java.util.HashSet;
import java.util.Set;
@Entity
public class Answer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String answerText;
private String postedBy;
@ManyToMany(mappedBy = "answers", fetch = FetchType.LAZY)
private Set<Question> questions = new HashSet<>();
protected Answer() { }
public Answer(String answerText, String postedBy) {
this.answerText = answerText;
this.postedBy = postedBy;
}
public Set<Question> getQuestions() { return questions; }
public String getAnswerText() { return answerText; }
public String getPostedBy() { return postedBy; }
}4. Configure Hibernate
Place hibernate.cfg.xml in src/main/resources and register both entities.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"https://hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<property name="hibernate.connection.driver_class">org.h2.Driver</property>
<property name="hibernate.connection.url">jdbc:h2:mem:forum-many;DB_CLOSE_DELAY=-1</property>
<property name="hibernate.connection.username">sa</property>
<property name="hibernate.connection.password"></property>
<property name="hibernate.dialect">org.hibernate.dialect.H2Dialect</property>
<property name="hibernate.hbm2ddl.auto">create-drop</property>
<property name="hibernate.show_sql">true</property>
<mapping class="com.example.Question"/>
<mapping class="com.example.Answer"/>
</session-factory>
</hibernate-configuration>Use database migrations instead of create-drop for production environments.
5. Persist Questions and Answers
Two questions and four answers are created. The same answer can be associated with more than one question, which demonstrates the many-to-many relationship.
try (SessionFactory factory = new Configuration()
.configure()
.buildSessionFactory();
Session session = factory.openSession()) {
Transaction transaction = session.beginTransaction();
try {
Answer javaAnswer = new Answer(
"Java is a programming language", "Ravi Malik");
Answer platformAnswer = new Answer(
"Java is a platform", "Sudhir Kumar");
Question java = new Question("What is Java?");
java.addAnswer(javaAnswer);
java.addAnswer(platformAnswer);
Question platform = new Question("Why is Java portable?");
platform.addAnswer(platformAnswer);
session.persist(java);
session.persist(platform);
transaction.commit();
} catch (RuntimeException exception) {
transaction.rollback();
throw exception;
}
}Because the question side cascades persist and merge, the new answers are persisted when the questions are persisted. The shared platformAnswer receives two join-table rows.
6. Fetch the Relationship with HQL
Use a fetch join when a screen needs the questions and their answers together.
List<Question> questions = session
.createQuery(
"select distinct q "
+ "from Question q "
+ "left join fetch q.answers",
Question.class)
.getResultList();
for (Question question : questions) {
System.out.println(question.getQuestionText());
for (Answer answer : question.getAnswers()) {
System.out.println(
answer.getAnswerText() + " - "
+ answer.getPostedBy());
}
}The collection should be accessed while the session is open. For pagination or large collections, use a separate query or an explicit fetch plan rather than joining everything into one result.
Using a List Instead of a Set
A list can be used when the association has meaningful order, but the order must be mapped explicitly.
@ManyToMany @JoinTable(name = "question_answer") @OrderColumn(name = "answer_position") private List<Answer> answers = new ArrayList<>();
Without an order column, database row order is not guaranteed. A set is usually the simpler choice when only membership matters.
When to Use a Join Entity
Use an explicit entity for the join table when the association has its own data, such as:
- When the answer was added to the question
- Who approved or voted for the association
- A display order or relevance score
- Status, permissions, or audit information
In that design, map two one-to-many relationships to an entity such as QuestionAnswer instead of hiding meaningful columns in a direct many-to-many table.
Many-to-Many Best Practices
- Use
Setwhen duplicate associations are invalid. - Define
equals()andhashCode()carefully for set elements. - Maintain both sides with helper methods.
- Do not use
CascadeType.REMOVEwhen the related entity can be shared. - Keep many-to-many collections lazy and fetch them deliberately.
- Add indexes and unique constraints to the join table.
- Do not include relationship collections in
equals()orhashCode(). - Use a join entity when the relationship itself has attributes.
question_answer join table.