Enterprise Computing · Week 3

Managing Persistent Objects

Relationship Mapping · Inheritance Strategies · EntityManager · Cascading
Part I Relationship & Inheritance Mapping
Part II Managing Persistent Objects

What We Cover Today

Part I — Relationships & Inheritance

🔗 Many-to-Many Bidirectional

Owner / inverse sides, join tables, @JoinTable

⚡ Fetching Strategies

Eager vs. Lazy loading and performance impact

🌳 Inheritance Mapping

Single-Table · Joined-Subclass · Mapped Superclass

Part II — Managing Persistent Objects

🎯 EntityManager

Central service for all persistence operations

💾 Persistence Context

First-level cache · lifecycle states · persistence.xml

🔄 CRUD + Cascading

Persist, find, remove, merge, flush, cascade events

Part I

Relationship & Inheritance Mapping

Many-to-Many Bidirectional  ·  Fetching Strategies  ·  Inheritance Strategies

Many-to-Many Bidirectional

Concept

When does it apply? When both entities hold a collection referencing the other.

📀 Example: Artist & CD Album

  • An Artist appears on many CDs
  • A CD features many Artists

In the relational world, the only way to represent this is with a join table.

💡 JPA needs to know which entity owns the relationship. The owner controls the join table. The inverse side uses mappedBy.

Here, Artist is the owner → Artist controls the join table mapping.

Artist id: Long List<CD> cds ARTIST_CD artist_fk cd_fk CD id: Long List<Artist> artists OWNER inverse

Many-to-Many Bidirectional

Code

Artist — Owner Side

@Entity
public class Artist {

  @ManyToMany
  @JoinTable(
    name = "ARTIST_CD",
    joinColumns =
      @JoinColumn(name="artist_fk"),
    inverseJoinColumns =
      @JoinColumn(name="cd_fk")
  )
  private List<CD> appearsOnCDs;
}
@JoinTable names the join table and maps both foreign key columns. Only the owner configures this.

CD — Inverse Side

@Entity
public class CD {

  @ManyToMany(
    mappedBy = "appearsOnCDs"
  )
  private List<Artist> artists;

}

🔑 Key Rule

mappedBy = the attribute name in the owning entity. CD is saying: "the other side already maps this."

Fetching Strategies

Eager vs Lazy

All relationship annotations accept a fetch attribute that controls when associated data is loaded.

⚡ EAGER

Load the related objects immediately when the parent entity is loaded. Everything arrives in one query.

@OneToMany(fetch = FetchType.EAGER)
private List<OrderLine> lines;
⚠️ Can cause N+1 queries or heavy loads if collections are large

💤 LAZY

Only load when you explicitly access the relationship. Nothing extra loaded at first.

@OneToMany(fetch = FetchType.LAZY)
private List<OrderLine> lines;
✅ Better performance — only fetch what you need, when you need it

JPA Defaults

AnnotationDefault FetchRationale
@ManyToOneEAGERSingle object — cheap to load
@OneToOneEAGERSingle object — cheap to load
@OneToManyLAZYCould be a large collection
@ManyToManyLAZYCould be a large collection

Inheritance Mapping

Overview

OOP uses inheritance naturally. Relational databases don't. JPA bridges the gap with four strategies:

📋

Single-Table

All classes → one flat table with a discriminator column

Default
🔗

Joined-Subclass

Each class gets its own table, joined by primary key

📄

Table-per-Class

Each concrete class gets its own complete table

Optional

🧬 Mapped Superclass

Not a true strategy — the superclass is not an entity. It only shares state/mapping to its subclasses. Cannot be queried or participate in relationships.

The @Inheritance(strategy = InheritanceType.XXX) annotation is placed on the root entity.

Single-Table Strategy

InheritanceType.SINGLE_TABLE

How It Works

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;
}
⚠️ Child-specific columns must be nullable — a CD row has no ISBN, so that column is NULL.

Resulting Table: ITEM

DTYPEIDTITLEISBNDURATION
Book1Clean Code978-...NULL
CD2Kind of BlueNULL45.2
Book3Refactoring978-...NULL

Trade-offs

  • ✅ Fast — no joins needed
  • ✅ Simple schema
  • ❌ Many nullable columns
  • ❌ Poor data integrity

Joined-Subclass Strategy

InheritanceType.JOINED

How It Works

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
}
To reassemble a Book, JPA joins ITEM and BOOK tables on their primary key. Deeper hierarchies = more joins = potential performance cost.

Resulting Tables

ITEM (root)

ID (PK)DTYPETITLEPRICE
1BookClean Code49.99
2CDKind of Blue12.99

BOOK

ID (FK→ITEM)ISBNNB_OF_PAGE
1978-0132...464

CD

ID (FK→ITEM)MUSIC_CODURATION
2Columbia46.4

Mapped Superclass

@MappedSuperclass

What Makes It Different?

❌ NOT an entity

The superclass has @MappedSuperclass, NOT @Entity. It has no table of its own and cannot be queried.

✅ Shares state & mapping

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;
}

Result: Only Subclass Tables

BOOK table (inherits Item's columns)

IDTITLEPRICEISBN
1Clean Code49.99978-0132...

CD table (inherits Item's columns)

IDTITLEPRICEDURATION
1Kind of Blue12.9946.4
⚠️ Cannot use @Table on the superclass. No shared base table — each entity is fully independent.

Inheritance Strategy Comparison

Decision Guide
Strategy Tables Nullable cols Query perf. Best for
Single-Table
SINGLE_TABLE
1 (root) Many NULLs Fast — no joins Small hierarchies, read-heavy
Joined-Subclass
JOINED
1 per class None Joins needed Normalised schema, data integrity
Table-per-Class
TABLE_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
💡 Rule of thumb: Use Joined-Subclass when data integrity matters; Single-Table when performance is the priority and the hierarchy is shallow; Mapped Superclass when you don't need to query the parent type directly.

✏️ Check Your Understanding — Part I

In a Many-to-Many bidirectional relationship, which entity controls the join table configuration?

A The inverse entity, using @JoinTable
B The owning entity, using @JoinTable and @JoinColumn
C Both entities share the configuration equally
D JPA auto-generates the join table — no annotation needed

Which inheritance strategy results in the MOST joins when querying a deep hierarchy?

A Single-Table
B Joined-Subclass
C Table-per-Class
D Mapped Superclass
Part II

Managing Persistent Objects

EntityManager  ·  Persistence Context  ·  CRUD Operations  ·  Cascading Events

The EntityManager

Central Service

The EntityManager is the core service through which all persistence operations flow.

What it does:

  • Creates, finds, updates, removes entities
  • Synchronises entity state with the database
  • Executes JPQL queries against entities
  • Provides locking for concurrent access
  • Manages the persistence context
📌 JPQL (Jakarta Persistence Query Language) looks like SQL but works with entity objects, not database tables directly.
// Example: JPQL vs SQL
// SQL:   SELECT * FROM CUSTOMER
// JPQL:
"SELECT c FROM Customer c"

Key Methods

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

Entity Lifecycle States

The Big Picture

An entity moves through four states during its lifetime. Understanding this is key to understanding persistence behaviour.

NEW
Created with new
Not tracked by EM
persist()
MANAGED
In persistence context
Auto-synced to DB
detach() / clear()
DETACHED
No longer tracked
Changes not saved
merge()
MANAGED
(re-attached)
remove() on MANAGED → REMOVED → deleted at commit
💡 When an entity is MANAGED, the EntityManager automatically detects changes to its fields and syncs them to the database at commit time — no explicit update() needed!
⚠️ DETACHED entities are still accessible as Java objects, but changes are silently lost unless you call em.merge().

Obtaining an EntityManager

Two Approaches

Application-Managed

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();

You must:

  • Manually begin() / commit() / rollback()
  • Open and close the EM and factory

Container-Managed

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 container handles transactions, commit, rollback, and EM lifecycle. Preferred in Jakarta EE apps.

Persistence Context

First-Level Cache

The Persistence Context is a set of managed entity instances associated with one transaction. Think of it as a first-level cache.

Key rules:

  • Only one instance per primary key in a given context
  • Lives for the duration of a transaction
  • Cleared when the transaction ends
If you call 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
Persistence Context (Tx scope) Book(id=12) MANAGED Book(id=56) MANAGED Book(id=34) MANAGED Cache cleared at tx end MySQL Database Synced at commit

The persistence.xml File

Configuration

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>

Key elements

  • <class> — lists every entity class that can be managed
  • transaction-typeRESOURCE_LOCAL (app-managed) or JTA (container-managed)
  • jdbc.* — database connection details
  • ddl-generation — controls whether tables are auto-created
💡 In a container-managed (JTA) environment, you use a <jta-data-source> instead of raw JDBC properties — the container manages the connection pool.

Persisting & Finding Entities

CRUD

Persisting an Entity

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
The INSERT doesn't happen immediately — it is batched until commit (or an explicit flush()). Until then, the entity exists only in the persistence context.

Finding by Primary Key

// 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
);

find() vs getReference()

  • find() — loads full entity data immediately, returns null if missing
  • getReference() — returns a lazy proxy; throws exception if data is needed after detachment

Removing Entities & Orphan Removal

CRUD

Removing an Entity

Customer c = em.find(
  Customer.class, 1L
);
em.remove(c); // MANAGED → REMOVED
tx.commit();  // SQL DELETE fires
⚠️ Without cascading, only the Customer row is deleted. The linked Address row becomes an orphan in the database!

Orphan Removal

@OneToOne(
  cascade = CascadeType.ALL,
  orphanRemoval = true
)
private Address address;

orphanRemoval = true triggers when:

  • The parent entity is removed
  • The relationship is broken (e.g., customer.setAddress(null))
Customer id=1, Alice Address id=1, 123 St FK ❌ removed ✅ auto-removed (orphanRemoval) Without orphanRemoval → Address is orphaned

Detach, Merge & Flush

Advanced Operations

detach()

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.

merge()

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!

flush()

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!
💡 Flush + rollback means the flush is reversed. The DB sees no change.

Cascading Events

Propagating Operations

By default, EntityManager operations apply only to the entity you pass in. Cascading lets operations propagate to related entities automatically.

Without Cascading

// Must persist BOTH manually:
Address a = new Address(...);
Customer c = new Customer(...);
c.setAddress(a);
em.persist(a); // explicit
em.persist(c); // explicit

With CascadeType.PERSIST

@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.

CascadeType Options

TypePropagates
PERSISTpersist() calls
REMOVEremove() calls
MERGEmerge() calls
REFRESHrefresh() calls
DETACHdetach() calls
ALLAll of the above

Works on:

@OneToOne, @OneToMany, @ManyToOne, @ManyToMany — all relationship annotations have a cascade attribute.

✏️ Check Your Understanding — Part II

You call em.clear() and then modify an entity. You call tx.commit(). What happens?

A The modification is saved — EntityManager tracks all Java objects
B The modification is lost — the entity was detached by clear()
C A DetachedEntityException is thrown at commit()
D The entity is automatically re-attached and saved

What does orphanRemoval = true do when you set customer.setAddress(null)?

A Nothing — only triggers when the parent Customer is removed
B Throws a NullPointerException
C Automatically deletes the Address entity at flush/commit time
D Sets the address FK to NULL in the database but keeps the Address row

Week 3 Summary

Key Takeaways

Part I — Relationships & Inheritance

🔗 Many-to-Many Bidirectional

Owner controls the join table via @JoinTable. Inverse side uses mappedBy.

⚡ Fetching

EAGER = load now. LAZY = load on access. Collections default to LAZY. Single refs default to EAGER.

🌳 Inheritance Strategies

Single-Table (fast, nullable cols) · Joined (normalised, joins needed) · Mapped Superclass (not an entity)

Part II — Managing Persistent Objects

🎯 EntityManager

Central service for persist, find, remove, merge, flush, detach. Container or application managed.

💾 Persistence Context

First-level cache. Entity states: NEW → MANAGED → DETACHED / REMOVED. Cleared at transaction end.

🔄 Cascading

Propagate persist/remove/merge to related entities automatically. CascadeType.ALL for everything.

🔬 Lab this week: Implement Joined-Subclass inheritance (Project 1) and Cascading Events with Customer/Address (Project 2)