Definition
- @ElementCollection -- maps a collection of basic values (
List) or@Embeddablevalue objects (List) that have no@Idand no@Entitystatus. - @CollectionTable -- specifies the separate table where the collection elements are stored, linked by a foreign key to the owning entity.
- Delete-all + reinsert -- on any modification (add, remove, reorder), Hibernate deletes all rows for the owner and reinserts the entire collection because elements have no identity to diff.
- @OneToMany -- maps a collection of child entities that have their own
@Id, enabling targeted INSERT, UPDATE, and DELETE on individual rows. - @OrderColumn -- preserves the order of a List by storing the index in a dedicated column in the collection table.
At National Bank of India, an account holder like Rajesh has a small list of registered phone numbers. @ElementCollection is like writing those phone numbers on a sticky note attached to the account file. If Rajesh adds one phone number, the clerk throws away the entire sticky note and writes a brand new one with all the numbers. For 3 phone numbers this is fine. But if we tried to store 10,000 transaction tags this way, the clerk would rewrite the entire list of 10,000 every time one tag changes, which is terrible. For large collections, we give each item its own file (@OneToMany) so the clerk can add or remove individual items.
Understanding the Concepts
What is @ElementCollection?
@ElementCollection maps a collection of basic values like List or Set, or a collection of @Embeddable value objects like List, that have no identity of their own. Unlike @OneToMany, the collected items are not entities. They have no @Id, no @Entity annotation, and no repository. They live and die with the owning entity.
- Elements are stored in a separate table specified by @CollectionTable.
- The table has a foreign key column pointing back to the owning entity.
- Default fetch type is LAZY, which is the correct default.
- The @Column annotation on the element specifies the column name for basic values.
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 = "holder_name", nullable = false)
private String holderName;
@Column(precision = 15, scale = 2)
private BigDecimal balance;
@ElementCollection(fetch = FetchType.LAZY)
@CollectionTable(
name = "account_phones",
joinColumns = @JoinColumn(name = "account_id")
)
@Column(name = "phone_number")
@OrderColumn(name = "phone_order")
private List<String> phoneNumbers = new ArrayList<>();
}
Generated table: account_phones ├── account_id VARCHAR(20) FK -> accounts(account_id) ├── phone_number VARCHAR(255) └── phone_order INT
How do you use @ElementCollection with @Embeddable value objects?
When the collection elements are composite values rather than simple strings, you wrap them in an @Embeddable class. Each field of the embeddable becomes a column in the collection table.
package com.bank.model;
import jakarta.persistence.Embeddable;
@Embeddable
public record Address(
String line1,
String city,
String state,
String pincode
) {}
package com.bank.model;
import jakarta.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "customers")
public class Customer {
@Id
@Column(name = "customer_id")
private String customerId;
private String name;
@ElementCollection
@CollectionTable(
name = "customer_addresses",
joinColumns = @JoinColumn(name = "customer_id")
)
private List<Address> addresses = new ArrayList<>();
}
Generated table: customer_addresses ├── customer_id VARCHAR(20) FK -> customers(customer_id) ├── line1 VARCHAR(255) ├── city VARCHAR(255) ├── state VARCHAR(255) └── pincode VARCHAR(255) Address has no @Id. It is a value object, not an entity.
Why is UPDATE expensive with @ElementCollection?
When you modify any element in the collection, Hibernate deletes ALL rows for the owner and reinserts the entire collection. This happens because elements have no @Id, so Hibernate cannot determine which specific row was added, removed, or changed. It has no way to diff the old and new collections at the row level.
package com.bank.service;
import com.bank.model.Account;
import com.bank.repository.AccountRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class PhoneService {
private final AccountRepository accountRepo;
public PhoneService(AccountRepository accountRepo) {
this.accountRepo = accountRepo;
}
@Transactional
public void addPhone(String accountId, String newPhone) {
Account account = accountRepo.findById(accountId).orElseThrow();
account.getPhoneNumbers().add(newPhone); // add ONE phone
}
// At flush, Hibernate generates:
// DELETE FROM account_phones WHERE account_id = 'SAV2001' (delete ALL)
// INSERT INTO account_phones VALUES ('SAV2001', '9876543210', 0)
// INSERT INTO account_phones VALUES ('SAV2001', '9876543211', 1)
// INSERT INTO account_phones VALUES ('SAV2001', '1234567890', 2) (new)
//
// For 3 phones: 1 DELETE + 3 INSERTs = acceptable
// For 10,000 tags: 1 DELETE + 10,001 INSERTs = terrible!
}
SQL generated for adding one phone to an account with 2 existing phones:
DELETE FROM account_phones WHERE account_id = 'SAV2001'
INSERT INTO account_phones (account_id, phone_number, phone_order) VALUES ('SAV2001', '9876543210', 0)
INSERT INTO account_phones (account_id, phone_number, phone_order) VALUES ('SAV2001', '9876543211', 1)
INSERT INTO account_phones (account_id, phone_number, phone_order) VALUES ('SAV2001', '1234567890', 2)
Total: 1 DELETE + 3 INSERTs just to add one phone number.
How is @ElementCollection different from @OneToMany?
| Aspect | @ElementCollection | @OneToMany |
|---|---|---|
| Element type | Basic value or @Embeddable | @Entity with @Id |
| Identity | No @Id, no identity | Has @Id, full identity |
| Own repository | No | Yes |
| Storage | @CollectionTable (separate table) | Child entity table |
| Update strategy | Delete-all + reinsert entire collection | Targeted INSERT/UPDATE/DELETE per row |
| Cascade / lifecycle | Implicitly owned (no cascade needed) | Requires CascadeType configuration |
| Querying elements | Cannot query elements independently | Can query child entities with JPQL |
| Performance at scale | Poor for large collections | Good for any collection size |
| Default fetch | LAZY | LAZY |
package com.bank.model;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
// @OneToMany approach: each transaction is a full entity
@Entity
@Table(name = "transactions")
public class Transaction {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id; // has its own @Id!
@Column(name = "account_id")
private String accountId;
@Column(precision = 15, scale = 2)
private BigDecimal amount;
@Column(name = "created_at")
private Instant createdAt;
}
// Adding one transaction: INSERT INTO transactions VALUES (...)
// Removing one transaction: DELETE FROM transactions WHERE id = 42
// No delete-all + reinsert. Each row is individually addressable.
What are common mistakes with @ElementCollection?
The most common mistakes involve using @ElementCollection for collections that are too large or too frequently changed.
// WRONG: Using @ElementCollection for a large, frequently changed collection
@Entity
public class Account {
@Id private String accountId;
@ElementCollection
@CollectionTable(name = "account_tags")
private Set<String> tags = new HashSet<>();
// If account has 10,000 tags and you add one:
// 1 DELETE + 10,001 INSERTs = catastrophic performance
}
// CORRECT: Use @OneToMany with a proper Tag entity
@Entity
public class AccountTag {
@Id @GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
@Column(name = "account_id")
private String accountId;
private String tag;
}
// Adding one tag: INSERT INTO account_tags (id, account_id, tag) VALUES (...)
// One statement, constant cost regardless of collection size.
// WRONG: Forgetting @CollectionTable (Hibernate uses default naming)
@ElementCollection
private List<String> phoneNumbers;
// Default table name: account_phonenumbers (may not match your schema)
// CORRECT: Always specify @CollectionTable explicitly
@ElementCollection
@CollectionTable(
name = "account_phones",
joinColumns = @JoinColumn(name = "account_id")
)
@Column(name = "phone_number")
private List<String> phoneNumbers;
When should you use @ElementCollection?
Use @ElementCollection when the collection is small (under 20 elements), rarely changes, elements are pure values without identity, and you do not need to query individual elements. Phone numbers, email addresses, roles, and small tag sets are good candidates.
// Good candidates for @ElementCollection:
// - Account phone numbers (2-3 per account, rarely change)
// - Customer email addresses (1-2 per customer)
// - User roles (3-5 roles, almost never change)
// - Account flags like "VIP", "FROZEN" (small set)
// Bad candidates (use @OneToMany instead):
// - Transaction history (thousands of entries, grows constantly)
// - Audit log entries (write-heavy, needs querying)
// - Order line items (need individual updates and queries)
When should you NOT use @ElementCollection?
Avoid @ElementCollection when the collection is large, frequently mutated, or when you need to query individual elements efficiently. The delete-all-reinsert behavior makes it unsuitable for high-volume or write-heavy scenarios.
package com.bank.service;
import com.bank.model.Account;
import com.bank.repository.AccountRepository;
import org.springframework.stereotype.Service;
@Service
public class PerformanceWarningDemo {
// Scenario: 10,000 transaction tags on one account
// Adding ONE tag with @ElementCollection:
// 1 DELETE + 10,001 INSERTs = ~10,002 SQL statements
// Estimated time: several seconds
// Adding ONE tag with @OneToMany (Tag entity):
// 1 INSERT = 1 SQL statement
// Estimated time: < 1 millisecond
// The performance difference is three to four orders of magnitude.
}
What about @ElementCollection with Set vs List?
Using a Set instead of List can improve the delete-reinsert behavior in some cases. With a Set, Hibernate can sometimes issue a targeted DELETE for the removed element instead of deleting all and reinserting, because Set elements are compared by value.
package com.bank.model;
import jakarta.persistence.*;
import java.util.HashSet;
import java.util.Set;
@Entity
@Table(name = "accounts")
public class AccountWithSetPhones {
@Id
@Column(name = "account_id")
private String accountId;
@ElementCollection
@CollectionTable(
name = "account_phones",
joinColumns = @JoinColumn(name = "account_id")
)
@Column(name = "phone_number")
private Set<String> phoneNumbers = new HashSet<>();
// Set: Hibernate MAY issue targeted DELETE for removed element
// List with @OrderColumn: always delete-all + reinsert
}
Visual Overview
@ElementCollection storage:
┌─────────────────────────┐ ┌──────────────────────────────┐
│ accounts │ │ account_phones │
│ ├── account_id (PK) │────>│ ├── account_id (FK) │
│ ├── holder_name │ │ ├── phone_number │
│ └── balance │ │ └── phone_order │
└─────────────────────────┘ └──────────────────────────────┘
Update flow (add one phone):
┌──────────────────────────────────────────────────┐
│ 1. account.getPhoneNumbers().add("9999999999") │
│ 2. Flush triggers: │
│ DELETE FROM account_phones │
│ WHERE account_id = 'SAV2001' (delete ALL) │
│ INSERT phone_1 (reinsert) │
│ INSERT phone_2 (reinsert) │
│ INSERT phone_3 (NEW phone) │
└──────────────────────────────────────────────────┘
@OneToMany storage:
┌─────────────────────────┐ ┌──────────────────────────────┐
│ accounts │ │ transactions │
│ ├── account_id (PK) │────>│ ├── id (PK) │
│ ├── holder_name │ │ ├── account_id (FK) │
│ └── balance │ │ ├── amount │
└─────────────────────────┘ │ └── created_at │
└──────────────────────────────┘
Update flow (add one transaction):
┌──────────────────────────────────────────────────┐
│ 1. INSERT INTO transactions (...) │
│ Only ONE statement. No delete-reinsert. │
└──────────────────────────────────────────────────┘
Key Takeaway
@ElementCollection maps a collection of basic values like phone numbers or @Embeddable value objects like addresses that have no identity of their own. JPA stores them in a separate @CollectionTable with a foreign key back to the owner. The critical limitation is that on any modification, Hibernate deletes all rows for the owner and reinserts the entire collection, because elements have no @Id and Hibernate cannot determine which specific row changed. This is acceptable for small, rarely changed collections but catastrophic for large or frequently mutated ones.
In our National Bank of India system, Rajesh's account stores 2-3 phone numbers as @ElementCollection. Adding one phone triggers a delete of all phones and reinsert of the updated list, which for 3 phones means 4 SQL statements and is perfectly fine. But the transaction history uses @OneToMany with a proper Transaction entity because adding one transaction should be a single INSERT, not a rewrite of thousands of rows.
The decision framework is straightforward: use @ElementCollection for small collections under 20 elements that rarely change and do not need independent querying. Use @OneToMany for anything large, frequently mutated, or needing individual CRUD. Using a Set instead of a List can sometimes allow targeted deletes, but the fundamental limitation remains.
Remember one sentence: @ElementCollection is simple and cheap for small value lists, but its delete-all-reinsert behavior makes it unsuitable for large or frequently changed collections where @OneToMany with proper entities is the right choice.
---