Hibernate Relationships

Hibernate One-to-Many Example Using Annotations

Map one Question to many Answer entities using a modern Jakarta Persistence and Hibernate annotation mapping.

One-to-Many Association

In a forum, one question can have many answers, while each answer belongs to one question. The parent entity contains a collection of answers and the child entity owns the foreign key.

Question1────────*Answer

This example uses a bidirectional relationship. Answer.question is the owning side because it contains the question_id foreign key. Question.answers uses mappedBy to refer to that field.

1. Add Maven Dependencies

Hibernate 6 uses the Jakarta Persistence namespace. H2 is used for a self-contained example; replace it with the JDBC driver for your database when needed.

<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>

Older Hibernate 5 applications may use javax.persistence, but the entity imports, Hibernate version, and dependency namespace must remain consistent.

2. Create the Question Entity

The parent entity owns the collection in the object model, while mappedBy tells Hibernate that the child controls the database relationship.

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.OneToMany;
import java.util.ArrayList;
import java.util.List;

@Entity
public class Question {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String questionText;

    @OneToMany(
        mappedBy = "question",
        cascade = CascadeType.ALL,
        orphanRemoval = true
    )
    private List<Answer> answers = new ArrayList<>();

    protected Question() { }

    public Question(String questionText) {
        this.questionText = questionText;
    }

    public void addAnswer(Answer answer) {
        answers.add(answer);
        answer.setQuestion(this);
    }

    public void removeAnswer(Answer answer) {
        answers.remove(answer);
        answer.setQuestion(null);
    }

    public Long getId() { return id; }
    public String getQuestionText() { return questionText; }
    public List<Answer> getAnswers() { return answers; }
}

3. Create the Answer Entity

The child entity contains the foreign-key mapping. FetchType.LAZY avoids loading every answer when only a question is needed.

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.JoinColumn;
import jakarta.persistence.ManyToOne;

@Entity
public class Answer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String answerText;
    private String postedBy;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "question_id", nullable = false)
    private Question question;

    protected Answer() { }

    public Answer(String answerText, String postedBy) {
        this.answerText = answerText;
        this.postedBy = postedBy;
    }

    public void setQuestion(Question question) {
        this.question = question;
    }

    public String getAnswerText() { return answerText; }
    public String getPostedBy() { return postedBy; }
}

4. Configure Hibernate

Place this file in src/main/resources/hibernate.cfg.xml. Both entities are registered explicitly.

<?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;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 Flyway, Liquibase, or another migration tool instead of create-drop for production schema management.

5. Persist Questions and Answers

Use the helper method to update both sides of the relationship. Because cascade = CascadeType.ALL is configured, persisting a question also persists its answers.

package com.example;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;

public class StoreData {
    public static void main(String[] args) {
        try (SessionFactory factory = new Configuration()
                .configure()
                .buildSessionFactory();
             Session session = factory.openSession()) {

            Transaction transaction = session.beginTransaction();
            try {
                Question java = new Question("What is Java?");
                java.addAnswer(new Answer(
                    "Java is a programming language", "Ravi Malik"));
                java.addAnswer(new Answer(
                    "Java is a platform", "Sudhir Kumar"));

                Question servlet = new Question("What is a Servlet?");
                servlet.addAnswer(new Answer(
                    "A Servlet is an interface", "Jai Kumar"));
                servlet.addAnswer(new Answer(
                    "A Servlet is an API", "Arun"));

                session.persist(java);
                session.persist(servlet);
                transaction.commit();
                System.out.println("Questions and answers saved");
            } catch (RuntimeException exception) {
                transaction.rollback();
                throw exception;
            }
        }
    }
}

6. Fetch the Relationship with HQL

Use a fetch join when the page needs questions and answers together. The distinct keyword avoids duplicate parent objects caused by the SQL join.

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());
    }
}

Alternatively, access the lazy collection inside an active transaction. Avoid exposing lazy entities directly from a closed-session web request.

Owning Side and Common Options

OptionPurpose
mappedBy = "question"Marks the parent collection as the inverse side of the relationship.
@JoinColumnDefines the foreign-key column on the child table.
cascade = CascadeType.ALLPropagates persistence and removal operations from question to answers.
orphanRemoval = trueDeletes a child removed from the parent collection.
@OrderColumnStores list position when answer order is meaningful.
Lifecycle warning: use orphanRemoval only when an answer cannot exist independently of its question. Do not use broad cascading on relationships whose child lifecycle is shared elsewhere.

Best Practices

  • Use jakarta.persistence with Hibernate 6.
  • Initialize collections and maintain both sides with helper methods.
  • Use LAZY for large collections and fetch deliberately.
  • Use Set when duplicates are invalid and List with @OrderColumn when position matters.
  • Exclude relationships from equals() and hashCode() to avoid recursion and lazy loading.
  • Use database indexes on foreign-key columns.
  • Use schema migrations in production.
Result: one question is stored in the question table, many answers are stored in the answer table, and each answer references its question through question_id.