Hibernate Entity Relationships: A Complete Guide to Object Associations

Introduction

Entity associations define how two or more entities relate to each other based on database relationship semantics. Hibernate provides four primary annotations to model these relationships:

  • @ManyToOne
  • @OneToMany
  • @OneToOne
  • @ManyToMany

@ManyToOne Relationship

The @ManyToOne annotation establishes a relationship where multiple child entities reference a single parent entity. This is similar to a foreign key constraint in relational databases.

@Entity(name = "Customer")
public static class Customer {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
}

@Entity(name = "Order")
public static class Order {
    @Id
    @GeneratedValue
    private Long id;
    
    private String orderNumber;
    
    @ManyToOne
    @JoinColumn(name = "customer_id",
        foreignKey = @ForeignKey(name = "CUSTOMER_ID_FK")
    )
    private Customer customer;
}

When persisting related entities:

@Test
public void persistOrderWithCustomer() {
    Customer customer = new Customer();
    session.save(customer);
    
    Order order = new Order();
    order.setOrderNumber("ORD-001");
    order.setCustomer(customer);
    
    session.save(order);
    transaction.commit();
}

Generated SQL:

INSERT INTO Customer (id) VALUES (1)
INSERT INTO Order (orderNumber, customer_id, id) VALUES ('ORD-001', 1, 2)

@OneToMany Relationship

The @OneToMany relationship indicates that a parent entity can have one or more associated child entities. This relationship can be implemented in two ways:

Unidirectional @OneToMany

In unidirectional associations, the child entity does not have a corresponding reference back to the parent. A join table is required, and removing child entities can be problematic.

@Entity(name = "Customer")
public static class Customer {
    @Id
    @GeneratedValue
    private Long id;
    
    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Order> orders = new ArrayList<>();
}

@Entity(name = "Order")
public static class Order {
    @Id
    @GeneratedValue
    private Long id;
    
    @Column(name = "order_number")
    private String orderNumber;
}

When saving a parent with children:

@Test
public void saveCustomerWithOrders() {
    Customer customer = new Customer();
    
    Order order1 = new Order("ORD-100");
    Order order2 = new Order("ORD-200");
    
    customer.getOrders().add(order1);
    customer.getOrders().add(order2);
    
    session.save(customer);
    transaction.commit();
}

Hibernate automatical creates a join table:

CREATE TABLE customer_orders (
    customer_id BIGINT NOT NULL,
    orders_id BIGINT NOT NULL
)

ALTER TABLE customer_orders 
    ADD CONSTRAINT FK_customer_orders_customer 
    FOREIGN KEY (customer_id) REFERENCES customer(id)

ALTER TABLE customer_orders 
    ADD CONSTRAINT FK_customer_orders_order 
    FOREIGN KEY (orders_id) REFERENCES orders(id)

Deleting a parent cascade deletes all associated children:

DELETE FROM customer_orders WHERE customer_id=?
DELETE FROM orders WHERE id=?
DELETE FROM customer WHERE id=?

However, direct child deletion is not supported in unidirectional associations and will throw a foreign key constraint violation.

Bidirectional @OneToMany

Bidirectional associations include a corresponding @ManyToOne on the child side, eliminating the need for a join table. The parent side uses mappedBy to indicate it's the non-owning side.

@Entity(name = "Customer")
public static class Customer {
    @Id
    @GeneratedValue
    private Long id;
    
    @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Order> orders = new ArrayList<>();
    
    public void addOrder(Order order) {
        orders.add(order);
        order.setCustomer(this);
    }
    
    public void removeOrder(Order order) {
        orders.remove(order);
        order.setCustomer(null);
    }
}

@Entity(name = "Order")
public static class Order {
    @Id
    @GeneratedValue
    private Long id;
    
    @NaturalId
    @Column(name = "order_number", unique = true)
    private String orderNumber;
    
    @ManyToOne
    private Customer customer;
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Order order = (Order) o;
        return Objects.equals(orderNumber, order.orderNumber);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(orderNumber);
    }
}

Deleting a child entity is straightforward:

DELETE FROM Order WHERE id=?

@OneToOne Relationship

The @OneToOne relationship links exactly one instance of each entity. The child side controls the relationship, similar to @ManyToOne.

Unidirectional @OneToOne

@Entity(name = "Product")
public static class Product {
    @Id
    @GeneratedValue
    private Long id;
    
    private String name;
    
    @OneToOne
    @JoinColumn(name = "specs_id")
    private ProductSpecs specs;
}

@Entity(name = "ProductSpecs")
public static class ProductSpecs {
    @Id
    @GeneratedValue
    private Long id;
    
    private String manufacturer;
    private String category;
}

Bidirectional @OneToOne

@Entity(name = "Product")
public static class Product {
    @Id
    @GeneratedValue
    private Long id;
    
    private String name;
    
    @OneToOne(
        mappedBy = "product",
        cascade = CascadeType.ALL,
        orphanRemoval = true,
        fetch = FetchType.LAZY
    )
    private ProductSpecs specs;
    
    public void addSpecs(ProductSpecs specs) {
        specs.setProduct(this);
        this.specs = specs;
    }
    
    public void removeSpecs() {
        if (specs != null) {
            specs.setProduct(null);
            this.specs = null;
        }
    }
}

@Entity(name = "ProductSpecs")
public static class ProductSpecs {
    @Id
    @GeneratedValue
    private Long id;
    
    private String manufacturer;
    private String category;
    
    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "product_id")
    private Product product;
}

@ManyToMany Relationship

The @ManyToMany relasionship requires a join table to represent the association between entities. Similar to @OneToMany, it can be unidirectional or bidirectional.

Unidirectional @ManyToMany

@Entity(name = "Student")
public static class Student {
    @Id
    @GeneratedValue
    private Long id;
    
    @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
    private List<Course> courses = new ArrayList<>();
}

@Entity(name = "Course")
public static class Course {
    @Id
    @GeneratedValue
    private Long id;
    
    private String title;
    private String department;
}

Modifying collections triggers operations on the join table:

@Test
public void updateStudentCourses() {
    Student student = session.get(Student.class, 15L);
    student.getCourses().remove(1);
    session.update(student);
    transaction.commit();
}

Bidirectional @ManyToMany

@Entity(name = "Student")
public static class Student {
    @Id
    @GeneratedValue
    private Long id;
    
    @NaturalId
    private String studentId;
    
    @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
    private List<Course> courses = new ArrayList<>();
    
    public void enrollInCourse(Course course) {
        courses.add(course);
        course.getStudents().add(this);
    }
    
    public void dropCourse(Course course) {
        courses.remove(course);
        course.getStudents().remove(this);
    }
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return Objects.equals(studentId, student.studentId);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(studentId);
    }
}

@Entity(name = "Course")
public static class Course {
    @Id
    @GeneratedValue
    private Long id;
    
    private String title;
    private String department;
    
    @ManyToMany(mappedBy = "courses")
    private List<Student> students = new ArrayList<>();
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Course course = (Course) o;
        return Objects.equals(title, course.title) &&
               Objects.equals(department, course.department);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(title, department);
    }
}

Converting @ManyToMany to @OneToMany

Sometimes bidirectional @ManyToMany associations are less efficient for delete and update operations. A common pattern is to convert it to bidirectional @OneToMany using an intermediate entity:

@Entity(name = "Student")
public static class Student implements Serializable {
    @Id
    @GeneratedValue
    private Long id;
    
    @NaturalId
    private String studentId;
    
    @OneToMany(
        mappedBy = "student",
        cascade = CascadeType.ALL,
        orphanRemoval = true
    )
    private List<StudentCourse> courses = new ArrayList<>();
    
    public void enrollInCourse(Course course) {
        StudentCourse sc = new StudentCourse(this, course);
        courses.add(sc);
        course.getEnrollments().add(sc);
    }
    
    public void dropCourse(Course course) {
        StudentCourse sc = new StudentCourse(this, course);
        course.getEnrollments().remove(sc);
        courses.remove(sc);
        sc.setStudent(null);
        sc.setCourse(null);
    }
}

@Entity(name = "StudentCourse")
public static class StudentCourse implements Serializable {
    @Id
    @ManyToOne
    private Student student;
    
    @Id
    @ManyToOne
    private Course course;
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        StudentCourse that = (StudentCourse) o;
        return Objects.equals(student, that.student) &&
               Objects.equals(course, that.course);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(student, course);
    }
}

@Entity(name = "Course")
public static class Course implements Serializable {
    @Id
    @GeneratedValue
    private Long id;
    
    private String title;
    private String department;
    
    @OneToMany(
        mappedBy = "course",
        cascade = CascadeType.ALL,
        orphanRemoval = true
    )
    private List<StudentCourse> students = new ArrayList<>();
}

Cascade Operations

Cascade operations define how changes to one entity affect associated entities:

  • CascadeType.PERSIST: When saving the parent, also save child entities. Throws an exception if a child already exists in the database.
  • CascadeType.MERGE: When updating the parent, also update child entities.
  • CascadeType.REMOVE: When deleting the parent, also delete child entities.
  • CascadeType.REFRESH: When refreshing the parent from the database, also refresh child entities.
  • CascadeType.ALL: Combines all four cascade types.
@Entity(name = "Student")
public static class Student {
    @Id
    @GeneratedValue
    private Long id;
    
    @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
    private List<Course> courses = new ArrayList<>;
}

Without CascadeType.REMOVE, deleting a student only removes the join table entries:

DELETE FROM student_course WHERE student_id=?
DELETE FROM student WHERE id=?

With CascadeType.REMOVE, associated courses are also deleted:

DELETE FROM student_course WHERE student_id=?
DELETE FROM course WHERE id=?
DELETE FROM student WHERE id=?

Understanding Foreign Key Relationships

When an entity's field references the primary key of another entity, it represents a foreign key. The referenced entity is the parent (principal) entity, while the referencing entity is the child (dependent) entity.

Database relationships translate to:

  • One-to-One
  • One-to-Many / Many-to-One
  • Many-to-Many

Foreign key constraints determine how associated data is handled when the parent table changes:

  • Cascade: Automatically applies the same operation to child records
  • SET NULL: Removes references from child records
  • RESTRICT: Prevents operations on parent if children exist (default)

Tags: hibernate jpa ORM java entity-relationships

Posted on Fri, 25 Sep 2026 16:51:30 +0000 by Nunners