Definition
- @ElementCollection – maps a collection of BASIC types (List
) or @Embeddable value objects into a separate collection table that is fully owned by the parent entity. - @CollectionTable – names the collection table and specifies the join column back to the parent.
- @Embeddable value object – a class with no @Id that represents a concept with no independent identity, stored as part of its parent.
- @OneToMany – maps a relationship to independent entities that have their own @Id, their own table, and their own lifecycle.
- Delete-all-reinsert – Hibernate's behaviour when modifying an @ElementCollection without an @OrderColumn: it deletes all rows for the parent and reinserts the whole collection.
- @OrderColumn – gives each element a position column, allowing Hibernate to target individual rows for updates.
An account at National Bank of India has a list of phone numbers and a list of nominees. Phone numbers are just VALUES: +91-98765-43210 has no life of its own and you would never look up a phone number as a standalone record. That is @ElementCollection. Trades, on the other hand, are real records with their own id that auditors query directly. That is @OneToMany to an entity. Values live inside the account's filing folder; entities have their own folders.
Understanding the Concepts
What is @ElementCollection?
@ElementCollection maps a collection of basic types or @Embeddable value objects into a separate collection table that is fully owned by the parent entity. The elements have no @Id, no independent lifecycle, and cannot be queried on their own. They are loaded and saved entirely as part of the parent.
package com.bank.model;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "accounts")
public class Account {
@Id
@Column(name = "account_id")
private String accountId;
@Column(name = "balance", precision = 15, scale = 2)
private BigDecimal balance;
// Collection of BASIC values
@ElementCollection
@CollectionTable(name = "account_phones", joinColumns = @JoinColumn(name = "account_id"))
@Column(name = "phone")
private List<String> phoneNumbers = new ArrayList<>();
// Collection of EMBEDDABLE value objects
@ElementCollection
@CollectionTable(name = "account_nominees", joinColumns = @JoinColumn(name = "account_id"))
@OrderColumn(name = "position") // avoids delete-all-reinsert
private List<Nominee> nominees = new ArrayList<>();
}
package com.bank.model;
import jakarta.persistence.Embeddable;
@Embeddable
public class Nominee {
private String name;
private String relationship;
private int sharePercent;
// NO @Id -- it is a value, not an entity
public Nominee() {}
public Nominee(String name, String relationship, int sharePercent) {
this.name = name;
this.relationship = relationship;
this.sharePercent = sharePercent;
}
// getters
}
Generated tables:
- account_phones: account_id (FK), phone (VARCHAR)
- account_nominees: account_id (FK), position (INT), name, relationship, share_percent
- Neither table has its own primary key (unless @OrderColumn provides one)
What is @OneToMany?
@OneToMany maps a relationship to independent entities. Each child has its own @Id, its own table, and its own lifecycle. You can query the child directly, reference it from other entities, and manage it independently.
package com.bank.model;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "accounts")
public class Account {
@Id
@Column(name = "account_id")
private String accountId;
@OneToMany(mappedBy = "account", cascade = CascadeType.ALL)
private List<Trade> trades = new ArrayList<>();
}
package com.bank.model;
import jakarta.persistence.*;
import java.math.BigDecimal;
@Entity
@Table(name = "trades")
public class Trade {
@Id
@Column(name = "trade_id")
private String tradeId; // has its own identity
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "account_id")
private Account account;
@Column(name = "amount", precision = 15, scale = 2)
private BigDecimal amount;
}
Trade is an ENTITY because:
- It has its own @Id (tradeId)
- It has its own table (trades)
- Auditors query trades directly
- It has an independent lifecycle
How do @ElementCollection and @OneToMany compare?
| Aspect | @ElementCollection | @OneToMany |
|---|---|---|
| Child type | Basic type or @Embeddable | Entity with @Id |
| Own table | Collection table (no PK) | Entity table (has PK) |
| Identity | No @Id, no independent existence | Has @Id, queryable directly |
| Lifecycle | Owned entirely by parent | Independent lifecycle |
| Cascade | Automatic (always cascaded) | Configurable (CascadeType) |
| Sharing | Cannot be shared between parents | Can be shared / referenced |
| Update behaviour | Delete-all-reinsert (without @OrderColumn) | Individual row updates |
package com.bank.service;
import com.bank.model.Account;
import com.bank.model.Nominee;
import com.bank.model.Trade;
public class ComparisonDemo {
public void demonstrate(Account account) {
// @ElementCollection: nominees owned by account, no independent query
account.getNominees().add(new Nominee("Priya", "spouse", 50));
// Saving the account saves the nominee automatically
// @OneToMany: trades are independent entities
Trade trade = new Trade("T001", account, new java.math.BigDecimal("50000.00"));
// Trade can be queried: tradeRepository.findById("T001")
// Trade can exist independently of Account in other contexts
}
}
What is the delete-all-reinsert problem?
If the collection table has no primary key and no @OrderColumn, Hibernate cannot identify individual rows. On ANY modification, Hibernate performs a DELETE of all rows for that parent followed by a full REINSERT. For a nominee list of three, this is harmless. For a large value collection, it is a performance trap.
// WRONG: no @OrderColumn -> delete-all-reinsert on every change
@ElementCollection
@CollectionTable(name = "account_phones", joinColumns = @JoinColumn(name = "account_id"))
@Column(name = "phone")
private List<String> phoneNumbers = new ArrayList<>();
// Adding one phone number:
// DELETE FROM account_phones WHERE account_id = 'SAV2001'
// INSERT INTO account_phones (account_id, phone) VALUES ('SAV2001', '+91-11111')
// INSERT INTO account_phones (account_id, phone) VALUES ('SAV2001', '+91-22222')
// INSERT INTO account_phones (account_id, phone) VALUES ('SAV2001', '+91-33333') <- the new one
// CORRECT: @OrderColumn allows individual row updates
@ElementCollection
@CollectionTable(name = "account_phones", joinColumns = @JoinColumn(name = "account_id"))
@Column(name = "phone")
@OrderColumn(name = "position")
private List<String> phoneNumbers = new ArrayList<>();
// Adding one phone number:
// INSERT INTO account_phones (account_id, phone, position) VALUES ('SAV2001', '+91-33333', 2)
// Only ONE INSERT, no delete-reinsert!
When should you use @ElementCollection vs @OneToMany?
Ask: does this child have an identity, and would anyone ever load or query it on its own? If yes, it is an entity and use @OneToMany. If it is just descriptive data attached to one parent and never shared, it is a value object and use @ElementCollection.
package com.bank.model;
// Value object: no identity, owned by parent
@Embeddable
public class Nominee {
private String name;
private String relationship;
private int sharePercent;
// Nobody queries "all nominees" as first-class records
}
// Entity: has identity, queried independently
@Entity
public class Trade {
@Id
private String tradeId;
// Auditors query trades directly, settlement system references them
}
What are common mistakes?
The most common mistake is using @ElementCollection for a child that needs to be shared between parents. Value objects cannot be shared. The second mistake is forgetting @OrderColumn on a large element collection.
// WRONG: trying to share a value object between two parents
public class WrongExample {
public void share() {
Nominee priya = new Nominee("Priya", "spouse", 100);
account1.getNominees().add(priya);
account2.getNominees().add(priya); // WRONG: Priya is duplicated in both tables
// Each parent owns its own COPY. If you update one, the other is unaffected.
// If you need sharing, Nominee must be an @Entity with @ManyToMany
}
}
What about the default fetch type?
@ElementCollection defaults to LAZY, which is usually what you want. The collection is loaded only when you access it. If you need it eagerly, you can set fetch = FetchType.EAGER, but this is rarely correct for collections.
// Default: LAZY (correct for most cases)
@ElementCollection // defaults to FetchType.LAZY
@CollectionTable(name = "account_nominees")
private List<Nominee> nominees;
// When you need them: use JOIN FETCH in a query
@Query("SELECT a FROM Account a LEFT JOIN FETCH a.nominees WHERE a.accountId = :id")
Account findWithNominees(String id);
What should a senior engineer know?
A senior engineer knows about the delete-all-reinsert trap and always adds @OrderColumn to list-based element collections. They know that value objects cannot be shared between parents. They use @ElementCollection for small, parent-owned collections (phone numbers, tags, nominees) and @OneToMany for anything with its own identity.
package com.bank.model;
import jakarta.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
public class Account {
@Id
private String accountId;
// Senior pattern: @OrderColumn + small owned collection
@ElementCollection
@CollectionTable(name = "account_nominees", joinColumns = @JoinColumn(name = "account_id"))
@OrderColumn(name = "position")
private List<Nominee> nominees = new ArrayList<>();
// Entity relationship for independent children
@OneToMany(mappedBy = "account", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Trade> trades = new ArrayList<>();
}
Visual Overview
@ElementCollection (Value Objects)
============================================
accounts table account_nominees table
+------------------+ +-----------------------+
| account_id (PK) | | account_id (FK) ------+---> accounts.account_id
| holder_name | | position |
| balance | | name |
+------------------+ | relationship |
| share_percent |
+-----------------------+
NO @Id, no PK of its own
@OneToMany (Entity Relationship)
============================================
accounts table trades table
+------------------+ +-----------------------+
| account_id (PK) | | trade_id (PK) | <- has its own PK!
| holder_name | | account_id (FK) ------+---> accounts.account_id
| balance | | amount |
+------------------+ | status |
+-----------------------+
HAS @Id, queryable directly
Decision Rule:
Does the child have identity? Can it be queried alone?
YES -> @OneToMany (entity)
NO -> @ElementCollection (value object)
Key Takeaway
@ElementCollection maps a collection of basic types or embeddable value objects into a separate collection table that is fully owned by the parent entity. The elements have no identity, no primary key, and no independent lifecycle. @OneToMany is completely different: it relates the parent to independent entities that each have their own id, their own table, and their own lifecycle, and which can be queried directly. The decision rule Rajesh applies is to ask whether the child has an identity and whether anyone would ever load or query it on its own.
In the account profile at National Bank of India, phone numbers are a collection of plain strings and nominees are embeddable value objects, so both are @ElementCollection owned by the account. Trades have trade ids, are queried directly by the settlement and audit systems, and have their own status lifecycle, so they are entities mapped with @OneToMany.
The senior caution Pankaj always raises is the delete-all-reinsert behaviour. Without an @OrderColumn, Hibernate cannot identify individual rows, so any change to the collection deletes every row for that parent and reinserts the whole thing. The fix is adding @OrderColumn so each element has a position. He also remembers that value objects cannot be shared between parents, so the moment you want to share a child between two accounts, that child is really an entity.
Remember one sentence: @ElementCollection is for parent-owned value objects with no identity, @OneToMany is for independent entities with their own id, and always add @OrderColumn to avoid delete-all-reinsert.
---