Collections
|
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. |
Hibernate wraps every mapped collection field in its own persistent collection implementation
(PersistentBag, PersistentList, PersistentSet, PersistentMap) that tracks additions/removals for dirty
checking. This page covers the collection interfaces Hibernate supports, @ElementCollection for collections
of non-entity values, and the bag-semantics trade-off that trips up more people than any other collection
detail.
List, Set, Map, and sorted collections
| Declared type | Hibernate behavior |
|---|---|
|
Treated as a bag — see below — unless |
|
No duplicates, no defined order (unless |
|
A key/value association; the key column is declared with |
|
Ordered in memory via |
@ElementCollection
For a collection of basic values or embeddables that have no identity of their own — no @Entity, just a
value stored in its own table keyed by the owner’s foreign key:
@Entity
public class Book {
@ElementCollection
@CollectionTable(name = "book_tag", joinColumns = @JoinColumn(name = "book_id"))
@Column(name = "tag")
private Set<String> tags = new HashSet<>();
@ElementCollection
@CollectionTable(name = "book_translation", joinColumns = @JoinColumn(name = "book_id"))
@MapKeyColumn(name = "locale")
private Map<String, String> translatedTitles = new HashMap<>();
@ElementCollection
@CollectionTable(name = "book_review", joinColumns = @JoinColumn(name = "book_id"))
private List<@Embeddable Review> reviews = new ArrayList<>();
}
An @ElementCollection has no cascading options to configure — the contained values are always fully owned by
the parent entity (they cannot be shared, referenced from elsewhere, or outlive their owner), so every add/
remove is unconditionally reflected as an INSERT/DELETE on the collection table.
@OrderColumn, @OrderBy, and @MapKey*
-
@OrderColumn— persists a dedicated integer index column and reconstructs aListin exactly that order, supporting index-based operations (list.set(3, …)) without reordering the rest of the table. -
@OrderBy("title ASC")— asks the database to sort onSELECT, referencing entity attribute names (not column names); the order is not stored anywhere, only applied at read time. Cheaper to write (no index column to maintain) but list index operations are not meaningful, since re-fetching can reorder elements if the sort key changes. -
@MapKeyColumn/@MapKey/@MapKeyJoinColumn— the three ways to say what a `Map’s key comes from: a dedicated column value, another attribute of the value entity, or a to-one association used as the key.
Bag vs. list semantics — the performance trade-off
A plain List<T> with no @OrderColumn is a bag: an unordered collection that permits duplicates,
represented internally as PersistentBag. Bags are the cause of a specific, easy-to-hit runtime error: a
query cannot JOIN FETCH two bags of the same root entity in one query — Hibernate cannot tell which rows
of the Cartesian product belong to which parent-collection element once two unordered bags are combined, and
throws MultipleBagFetchException. The fixes, in order of preference:
-
Change one of the two collections to a
Set(if duplicates genuinely cannot occur) — the most common fix. -
Add
@OrderColumnto make it a true ordered list instead of a bag. -
Fetch the two collections in separate queries (or via
@BatchSize, see Fetching & N+1) instead of both in oneJOIN FETCH.
A Set also avoids a related, subtler bag cost: adding one element to a bag-mapped @OneToMany/
@ManyToMany collection sometimes forces Hibernate to delete and re-insert the entire collection’s join rows
to preserve correctness, because a bag has no identity per row to update selectively — another reason Set
(or an ordered List via @OrderColumn) is generally the safer default over a bare List.
Mapping to SQL arrays
Where the database and dialect support a native array column type (PostgreSQL’s text[], integer[], …), a
basic collection of a simple element type can map directly to it via @JdbcTypeCode(SqlTypes.ARRAY) on a
String[]/Integer[] field, avoiding a separate @ElementCollection table entirely for small, denormalized,
rarely-queried-by-element lists.