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)