Associations
|
This section documents Hibernate ORM 7.4.x (User Guide, Introduction, Query Language Guide, Data Repositories Guide), Jakarta Persistence 3.2, Hibernate Search 8.4.x, and the Hibernate Validator / Hibernate Reactive references — which are the references these pages are written and verified against. This content was generated with the assistance of AI and should be verified against those official docs before being relied on in production. Three older reference books were consulted as bibliography only while preparing these pages and are not the
primary or main source for any page. All three predate Jakarta Persistence 3.2 and Hibernate ORM 6/7 (the
This section’s bibliography lists the reference material consulted while preparing these pages. |
Associations are where the paradigm mismatch bites hardest: a directional Java reference has to become an undirected foreign key, navigable from either table. This page covers the four association shapes, the owning/inverse distinction every bidirectional association needs, and cascading.
@ManyToOne — the simple, owning case
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "author_id", nullable = false)
private Author author;
}
@ManyToOne always owns the foreign-key column (@JoinColumn) — there is nothing to configure about which
side is owning here, since only the "many" side has a column to hold the key.
@OneToMany — owning vs. inverse
The "one" side of a to-many association has no column of its own to store; it can only ever be the inverse
(non-owning) side, declared with mappedBy naming the owning @ManyToOne field on the other entity:
@Entity
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "author", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Book> books = new ArrayList<>();
// bidirectional-sync helper methods -- see below
public void addBook(Book book) {
books.add(book);
book.setAuthor(this);
}
public void removeBook(Book book) {
books.remove(book);
book.setAuthor(null);
}
}
Owning side is the side whose column changes actually get written — Hibernate only looks at the owning
side’s in-memory state to decide what SQL to issue. Mutating only the inverse side’s collection
(author.getBooks().add(book) without also setting book.setAuthor(author)) persists nothing: no
INSERT/UPDATE is generated, because the owning @ManyToOne field never changed. This is the single most
common bidirectional-association bug, and the reason for the helper-method pattern above: addBook/removeBook
keep both sides of the in-memory graph consistent, so callers never have to remember to touch both fields
themselves. It is also possible, and often simpler, to map an association unidirectionally — keep only the
owning @ManyToOne (or @OneToMany with an explicit @JoinColumn, avoiding mappedBy and the inverse
collection entirely) when the reverse navigation is never actually needed.
@OneToOne
Two shapes:
// (a) Foreign-key shared column, unidirectional/owning
@Entity
public class Employee {
@Id
private Long id;
@OneToOne
@JoinColumn(name = "parking_spot_id")
private ParkingSpot parkingSpot;
}
// (b) Shared primary key via @MapsId -- see entities-and-identifiers.adoc
@Entity
public class EmployeeDetails {
@Id
private Long id;
@MapsId
@OneToOne
@JoinColumn(name = "id")
private Employee employee;
}
Bidirectional @OneToOne needs the same mappedBy treatment as @OneToMany: the non-owning side declares
@OneToOne(mappedBy = "employee") and holds no column. See
Entities and Identifiers for the shared-primary-key
variant in full.
@ManyToMany and @JoinTable
@Entity
public class Book {
@ManyToMany
@JoinTable(
name = "book_category",
joinColumns = @JoinColumn(name = "book_id"),
inverseJoinColumns = @JoinColumn(name = "category_id"))
private Set<Category> categories = new HashSet<>();
}
@Entity
public class Category {
@ManyToMany(mappedBy = "categories")
private Set<Book> books = new HashSet<>();
}
A many-to-many needs a join table with no object-model counterpart — one more instance of the associations
half of the paradigm mismatch. When the link itself needs its own attributes (a timestamp, a role, a quantity),
@ManyToMany cannot express it; model the join table as its own @Entity with two @ManyToOne associations
instead (an explicit "link entity"), which is also the only shape that supports adding a link-specific
@Version column for optimistic locking on the association itself.
Cascading and orphan removal
cascade on an association propagates JPA operations from the owning entity to the associated one(s):
PERSIST, MERGE, REMOVE, REFRESH, DETACH, or ALL for all five. orphanRemoval = true (available on
@OneToOne/@OneToMany) goes further: removing a child from the collection, or reassigning a @OneToOne to a
different value, deletes the orphaned row — not just skips cascading a later remove() call, but actively
issues a DELETE for anything that fell out of the association. Reach for cascade = CascadeType.ALL,
orphanRemoval = true on a true parent/child ("composition") relationship whose children have no independent
existence or lifecycle outside their parent (order/order-line is the textbook case); avoid it on associations
between independently-lifecycled entities, where cascading a remove() would delete something the rest of the
system still expects to exist.
@OnDelete(action = OnDeleteAction.CASCADE) is a different mechanism entirely: it tells schema-generation
tooling to add ON DELETE CASCADE to the foreign-key constraint itself, so the database enforces the cascade
even for deletes that bypass Hibernate (a raw SQL DELETE, another application). It composes with, but does
not replace, JPA-level orphanRemoval/cascade = REMOVE, which only fire for deletes that go through
Hibernate’s own EntityManager.