Owner / inverse sides, join tables, @JoinTable
Eager vs. Lazy loading and performance impact
Single-Table · Joined-Subclass · Mapped Superclass
Central service for all persistence operations
First-level cache · lifecycle states · persistence.xml
Persist, find, remove, merge, flush, cascade events
When does it apply? When both entities hold a collection referencing the other.
In the relational world, the only way to represent this is with a join table.
mappedBy.
Here, Artist is the owner → Artist controls the join table mapping.
@Entity public class Artist { @ManyToMany @JoinTable( name = "ARTIST_CD", joinColumns = @JoinColumn(name="artist_fk"), inverseJoinColumns = @JoinColumn(name="cd_fk") ) private List<CD> appearsOnCDs; }
@Entity public class CD { @ManyToMany( mappedBy = "appearsOnCDs" ) private List<Artist> artists; }
mappedBy = the attribute name in the owning entity. CD is saying: "the other side already maps this."
All relationship annotations accept a fetch attribute that controls when associated data is loaded.
Load the related objects immediately when the parent entity is loaded. Everything arrives in one query.
@OneToMany(fetch = FetchType.EAGER) private List<OrderLine> lines;
Only load when you explicitly access the relationship. Nothing extra loaded at first.
@OneToMany(fetch = FetchType.LAZY) private List<OrderLine> lines;
| Annotation | Default Fetch | Rationale |
|---|---|---|
@ManyToOne | EAGER | Single object — cheap to load |
@OneToOne | EAGER | Single object — cheap to load |
@OneToMany | LAZY | Could be a large collection |
@ManyToMany | LAZY | Could be a large collection |
OOP uses inheritance naturally. Relational databases don't. JPA bridges the gap with four strategies:
All classes → one flat table with a discriminator column
DefaultEach class gets its own table, joined by primary key
Each concrete class gets its own complete table
OptionalNot a true strategy — the superclass is not an entity. It only shares state/mapping to its subclasses. Cannot be queried or participate in relationships.
@Inheritance(strategy = InheritanceType.XXX) annotation is placed on the root entity.
All entities in the hierarchy share one table. A special discriminator column (DTYPE by default) identifies which class each row belongs to.
@Entity @Inheritance(strategy = InheritanceType.SINGLE_TABLE) // @Inheritance can be omitted — it's the default public class Item { ... } @Entity public class Book extends Item { private String isbn; private Integer nbOfPage; } @Entity public class CD extends Item { private Float totalDuration; }
| DTYPE | ID | TITLE | ISBN | DURATION |
|---|---|---|---|---|
| Book | 1 | Clean Code | 978-... | NULL |
| CD | 2 | Kind of Blue | NULL | 45.2 |
| Book | 3 | Refactoring | 978-... | NULL |
Each entity in the hierarchy maps to its own table. Subclass tables contain only their own columns + a FK back to the root table.
@Entity @Inheritance(strategy = InheritanceType.JOINED) public class Item { @Id private Long id; private String title; private Float price; } @Entity public class Book extends Item { private String isbn; // id inherited from ITEM table }
Book, JPA joins ITEM and BOOK tables on their primary key. Deeper hierarchies = more joins = potential performance cost.
ITEM (root)
| ID (PK) | DTYPE | TITLE | PRICE |
|---|---|---|---|
| 1 | Book | Clean Code | 49.99 |
| 2 | CD | Kind of Blue | 12.99 |
BOOK
| ID (FK→ITEM) | ISBN | NB_OF_PAGE |
|---|---|---|
| 1 | 978-0132... | 464 |
CD
| ID (FK→ITEM) | MUSIC_CO | DURATION |
|---|---|---|
| 2 | Columbia | 46.4 |
The superclass has @MappedSuperclass, NOT @Entity. It has no table of its own and cannot be queried.
Fields and annotations (like @Column) in the superclass are inherited by subclass entities.
@MappedSuperclass // NOT @Entity! public abstract class Item { @Id protected Long id; protected String title; protected Float price; } @Entity // Book IS an entity public class Book extends Item { private String isbn; }
BOOK table (inherits Item's columns)
| ID | TITLE | PRICE | ISBN |
|---|---|---|---|
| 1 | Clean Code | 49.99 | 978-0132... |
CD table (inherits Item's columns)
| ID | TITLE | PRICE | DURATION |
|---|---|---|---|
| 1 | Kind of Blue | 12.99 | 46.4 |
@Table on the superclass. No shared base table — each entity is fully independent.
| Strategy | Tables | Nullable cols | Query perf. | Best for |
|---|---|---|---|---|
Single-TableSINGLE_TABLE |
1 (root) | Many NULLs | Fast — no joins | Small hierarchies, read-heavy |
Joined-SubclassJOINED |
1 per class | None | Joins needed | Normalised schema, data integrity |
Table-per-ClassTABLE_PER_CLASS |
1 per concrete class | None | UNION queries | Rarely used (optional) |
Mapped Superclass@MappedSuperclass |
1 per concrete entity | None | Fast | Shared state, no polymorphic queries |
In a Many-to-Many bidirectional relationship, which entity controls the join table configuration?
@JoinTable@JoinTable and @JoinColumnWhich inheritance strategy results in the MOST joins when querying a deep hierarchy?
The EntityManager is the core service through which all persistence operations flow.
// Example: JPQL vs SQL // SQL: SELECT * FROM CUSTOMER // JPQL: "SELECT c FROM Customer c"
em.persist(entity)Insert a new entity into the database
em.find(Class, id)Look up an entity by its primary key
em.remove(entity)Delete entity from the database
em.merge(entity)Re-attach a detached entity
em.flush() / refresh()Force sync / reload from database
An entity moves through four states during its lifetime. Understanding this is key to understanding persistence behaviour.
newupdate() needed!
em.merge().
Your code creates and controls the EntityManager.
EntityManagerFactory emf = Persistence.createEntityManagerFactory( "myPU" ); EntityManager em = emf.createEntityManager(); EntityTransaction tx = em.getTransaction(); tx.begin(); // ... your operations ... tx.commit(); em.close(); emf.close();
begin() / commit() / rollback()The Jakarta EE container injects and manages the EM for you.
@Stateless public class CustomerService { @PersistenceContext private EntityManager em; public void save(Customer c) { em.persist(c); // No commit needed! } }
The Persistence Context is a set of managed entity instances associated with one transaction. Think of it as a first-level cache.
em.find(Book.class, 1L) twice in one transaction, the database is only hit once. The second call returns the cached instance.
// Both users have their own context // User 1: loads Book ID=12 and ID=56 // User 2: loads Book ID=12 and ID=34 // ID=12 exists in BOTH contexts independently
The persistence unit is defined in src/main/resources/META-INF/persistence.xml. It bridges the persistence context to the database.
<!-- Application-Managed (RESOURCE_LOCAL) --> <persistence-unit name="myPU" transaction-type="RESOURCE_LOCAL"> <class>entities.Book</class> <class>entities.CD</class> <properties> <property name="jakarta.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver"/> <property name="jakarta.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/mydb"/> <property name="jakarta.persistence.jdbc.user" value="root"/> <property name="jakarta.persistence.jdbc.password" value="password"/> <property name="eclipselink.ddl-generation" value="create-tables"/> </properties> </persistence-unit>
RESOURCE_LOCAL (app-managed) or JTA (container-managed)<jta-data-source> instead of raw JDBC properties — the container manages the connection pool.
EntityManager em = ... ; EntityTransaction tx = em.getTransaction(); tx.begin(); Customer c = new Customer(); c.setFirstName("Alice"); c.setEmail("alice@example.com"); em.persist(c); // State: NEW → MANAGED tx.commit(); // SQL INSERT fires here
flush()). Until then, the entity exists only in the persistence context.
// find() — returns null if not found Customer c = em.find( Customer.class, 1L ); // getReference() — returns a proxy // LazyLoadingException if accessed // after detachment! Customer ref = em.getReference( Customer.class, 1L );
null if missingCustomer c = em.find( Customer.class, 1L ); em.remove(c); // MANAGED → REMOVED tx.commit(); // SQL DELETE fires
Customer row is deleted. The linked Address row becomes an orphan in the database!
@OneToOne( cascade = CascadeType.ALL, orphanRemoval = true ) private Address address;
customer.setAddress(null))Remove an entity from the persistence context. Changes after detach are not saved.
Customer c = em.find( Customer.class, 1L ); em.detach(c); c.setEmail("new@x.com"); // ↑ change is lost! tx.commit();
Use em.contains(c) to check if managed.
Re-attach a detached entity and copy its state back into the persistence context.
em.clear(); // detach all c.setEmail("new@x.com"); // Re-attach: Customer managed = em.merge(c); tx.commit(); // ↑ UPDATE fires
merge() returns the managed copy. Use the returned instance!Force immediate synchronisation with the DB before the transaction commits.
em.persist(c); em.flush(); // SQL INSERT now // Still inside tx: em.getTransaction() .rollback(); // INSERT is undone!
By default, EntityManager operations apply only to the entity you pass in. Cascading lets operations propagate to related entities automatically.
// Must persist BOTH manually: Address a = new Address(...); Customer c = new Customer(...); c.setAddress(a); em.persist(a); // explicit em.persist(c); // explicit
@OneToOne(cascade = CascadeType.PERSIST) private Address address; // Now only persist the Customer: em.persist(c); // Address auto-persisted!
CascadeType.ALL covers all events: PERSIST, REMOVE, MERGE, REFRESH, DETACH.
| Type | Propagates |
|---|---|
PERSIST | persist() calls |
REMOVE | remove() calls |
MERGE | merge() calls |
REFRESH | refresh() calls |
DETACH | detach() calls |
ALL | All of the above |
@OneToOne, @OneToMany, @ManyToOne, @ManyToMany — all relationship annotations have a cascade attribute.
You call em.clear() and then modify an entity. You call tx.commit(). What happens?
clear()DetachedEntityException is thrown at commit()What does orphanRemoval = true do when you set customer.setAddress(null)?
Owner controls the join table via @JoinTable. Inverse side uses mappedBy.
EAGER = load now. LAZY = load on access. Collections default to LAZY. Single refs default to EAGER.
Single-Table (fast, nullable cols) · Joined (normalised, joins needed) · Mapped Superclass (not an entity)
Central service for persist, find, remove, merge, flush, detach. Container or application managed.
First-level cache. Entity states: NEW → MANAGED → DETACHED / REMOVED. Cleared at transaction end.
Propagate persist/remove/merge to related entities automatically. CascadeType.ALL for everything.