ORM & JPA: From JDBC to the Entity Manager
Mapping objects to tables without writing SQL. The JDBC boilerplate JPA removes, entities and the persistence context, and the four decisions every exam asks you to justify from data cardinalities — owner, mapped-by, fetch policy and cascade — plus the entity lifecycle and container transactions.
01 · Context
Three-tier architectures and the JEE stack
A short orientation: where in an application the code we are about to write actually runs.
Distributed architecture went one-tier (1960s–70s) → client-server (70s–80s) → RPC-based (80s) → object-oriented distributed (90s) → web (late 90s) → today’s mix of service-oriented, mobile and cloud architectures.
Client-server puts business and presentation logic in the client, which sends SQL to a server that only manages data. Three-tier inserts a middle tier that centralizes connections to the data server, masks the data model from clients, and can be replicated to scale. In a pure-HTML web application the client is a thin browser, and the middle tier holds a web server, the business logic that generates content, and the presentation assembly.
The Java EE pieces that matter here: JDBC, the first industry-standard database-independent connectivity API — still important but superseded for our purposes; Servlets in the presentation tier; EJB for server-side business components with container services; JPA, the specification for mapping relational data to objects, including the JPQL query language; and JTA, which manages transactions in a resource-agnostic way and allows transactional properties to be declared rather than coded.
02 · Motivation
The JDBC boilerplate problem
Everything JPA does is best understood as the removal of specific, tedious code. Here is the code.
The course’s running example is an expense-report application: users log in, see their travel
missions, file expenses against one, and close it once reimbursed. A mission moves through three
states — open → reported → closed — and the schema is three tables with foreign keys.
Written directly against JDBC, four kinds of tedium appear.
Connection management. A ConnectionHandler loads the driver and opens a connection from
web.xml context parameters; every controller repeats init() and destroy() to acquire and release
it. Connections end up held one per controller as a data member, which is not efficient, and
forgetting to release one is costly.
Copying result rows into objects. Every query walks a ResultSet and copies column by column:
try (ResultSet result = pstatement.executeQuery()) {
while (result.next()) {
Mission mission = new Mission();
mission.setId(result.getInt("id"));
mission.setStartDate(result.getDate("date"));
mission.setDestination(result.getString("destination"));
// … one line per column, forever
}
}Copying objects back into statements. Inserts and updates do the same in reverse, binding each
field to a ? placeholder in order.
Manual transaction demarcation. Two updates that must be atomic require explicit control:
connection.setAutoCommit(false);
try {
pstatement.executeUpdate(); // 1st update
missionDAO.changeMissionStatus(id, REPORTED); // 2nd, must be atomic with it
connection.commit();
} catch (SQLException e) {
connection.rollback();
throw e;
} finally { connection.setAutoCommit(true); }The lectures then ask the obvious question — wouldn’t it be better if a query returned results already in the right application type, if creating an object created a tuple, if updating an object updated the matching row, if connections were dispatched automatically, and if methods joined transactions by themselves? — and answer it with a problem-solution matrix: ORM for the two copying problems, JTA plus dependency injection for connections, and container-managed transactions for demarcation.
03 · Foundation
Entity, persistence unit, persistence context, managed entity
Four definitions. They are asked verbatim, so learn them as a quotable set.
The underlying difficulty is impedance mismatch — concepts in one model with no direct equivalent in the other:
| Object model (Java) | Relational model |
|---|---|
| objects, classes | rows, tables |
| attributes, properties | columns |
| identity (memory address) | primary key |
| reference to another entity | foreign key |
| inheritance, polymorphism | not supported |
| methods | stored procedures, triggers |
JPA bridges it with a POJO persistence model. The four terms:
Entity
A class (JavaBean) representing a collection of persistent objects mapped onto a relational table.
Persistence unit
The set of all classes persistently mapped to one database — analogous to a database schema.
Persistence context
The set of all managed objects of the entities defined in the persistence unit — analogous to the database instance in use.
Managed entity
An entity that is part of a persistence context, and whose state changes are tracked for automatic synchronization to the database.
JPA — 6 of 14 sessions, 49 points
JPA exercises appear in 6 of the 14 papers, worth 9–10 points each. Five of the six are
relationship mapping; the sixth (2024-08-30) is pure theory and asks for exactly the four
definitions above, plus fetch modes and the notion of relationship owner. Note what the bank does
not
contain: no exercise has ever asked about the EntityManager lifecycle in depth, or about
@TransactionAttribute. Sections 8 is therefore covered properly but briefly.
An entity class carries a few hard requirements: a public or protected no-argument constructor;
the class must not be final, nor may any persistent field or method be; and it must implement
Serializable if instances are to be passed by value as detached objects.
04 · Mapping
Identity and attribute mapping
A POJO has no durable identity. Persistence gives it one, and it is the primary key.
@Entity
public class Mission implements Serializable {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@Temporal(TemporalType.DATE) private Date date;
@Enumerated(EnumType.STRING) private MissionStatus status;
@Basic(fetch = FetchType.LAZY) @Lob private byte[] photo;
@Transient private BigDecimal computedDiscount;
}@Id marks the simple primary key; composite keys use @EmbeddedId or @IdClass. @GeneratedValue
delegates key generation to the provider, with four strategies:
| Strategy | Meaning |
|---|---|
AUTO | the provider picks whatever it likes |
TABLE | identifiers come from a generator table |
SEQUENCE | uses the database’s sequence feature |
IDENTITY | uses identity/auto-increment columns |
The lectures’ advice is short: whenever possible, let the database generate the ids.
For attributes, the annotations worth knowing are @Temporal (DATE / TIME / TIMESTAMP), @Enumerated
(as STRING or as the ordinal), @Lob for large objects, @Transient for fields that are not
persisted, and @Table / @Column to override the default naming — by default an entity maps to a
table of the same name and each field to a column of the same name. @Column also carries schema
generation hints such as nullable, unique and length.
Always pair @Lob with lazy fetching
The published grader’s comments for January 2024 list “missing annotations for Blob fields” among
the common errors, noting that the typical annotations are among the goals for that part of the
exam. A photo or media file loaded eagerly on every query is a real performance bug, and the fix
is
@Lob @Basic(fetch = FetchType.LAZY). It is one line, and it is worth marks.
05 · Mapping
Relationships: directionality, cardinality, ownership
The centre of the chapter and of the exam. Every relationship has four characteristics, and getting ownership right is what most answers turn on.
- Directionality. All JPA relationships are unidirectional. A bidirectional relationship is a
matched pair of unidirectional mappings, and the matching must be declared explicitly with
mappedBy. - Role. One entity is the source, the other the target of each direction.
- Cardinality. Four combinations: many-to-one, one-to-many, one-to-one, many-to-many. A to-one direction means a single reference; a to-many means a collection.
- Ownership. One side owns the relationship.
The owner is whichever table physically holds the foreign key
In the database a relationship is implemented by a FK column — in JPA, a
join column. For 1:N and 1:1, the entity whose table stores that column is the
owner, and its side is the owning side. It matters for three reasons: the
physical-mapping annotations (@JoinColumn, @JoinTable) go on the owner;
the inverse side carries
mappedBy; and — the part that bites —
only changes made on the owning side are persisted.
That last point deserves the code, because it is counter-intuitive:
// OWNING side — Employee holds department_id. Generates UPDATE EMPLOYEE SET department_id = 3 …
emp.setDepartment(newDept);
em.persist(emp);
// INVERSE side — no SQL is generated at all.
dept.getEmployees().add(emp);
em.persist(dept);The mappings themselves:
// 1:N bidirectional — the MANY side is always the owner
@Entity class Employee {
@ManyToOne @JoinColumn(name = "dept_fk") // owner: its table holds the FK
private Department dept;
}
@Entity class Department {
@OneToMany(mappedBy = "dept") // inverse: points at the owning attribute
private Collection<Employee> employees;
}
// 1:1 — either side may own; the one with the FK does
@Entity class Employee {
@OneToOne @JoinColumn(name = "P_SPACE_FK") private ParkingSpace parkingSpace;
}
@Entity class ParkingSpace {
@OneToOne(mappedBy = "parkingSpace") private Employee employee;
}
// N:M — no FK column exists, so a join table is used and either side may own
@Entity class Employee {
@ManyToMany
@JoinTable(name = "EMP_PROJ",
joinColumns = @JoinColumn(name = "EMP_ID"),
inverseJoinColumns = @JoinColumn(name = "PROJ_ID"))
private Collection<Project> projects;
}
@Entity class Project {
@ManyToMany(mappedBy = "projects") private Collection<Employee> employees;
}Two defaults save typing. Without @JoinColumn, a to-one mapping’s column is named
<attribute>_<target PK column> — parkingspace_id. Without @JoinTable, an N:M join table is named
<Owner>_<Inverse> with columns <Owner>_id and <Inverse>_id.
mappedBy is what stops JPA creating a bridge table
Omit mappedBy on a 1:N and the default mapping does not silently fall back to the FK
— it creates a bridge table, exactly as for N:M. So mappedBy is
doing two jobs: declaring which side is inverse, and telling JPA “the relationship is already
mapped by a FK in the other entity, do not build a join table”. The grader’s comments for January
2024 flag
@JoinTable/@JoinColumn/mappedBy placed on the wrong side
relative to the declared owner as a recurring error, with a penalty when the owner was not stated
explicitly at all.
One more annotation appears in nearly every exam solution and is easy to forget:
@OrderBy("creationdate DESC") on a collection, whenever the application shows that collection in a
particular order. Missing @OrderBy is the first item on January 2024’s list of common errors.
06 · Mapping
Fetch policies
When you load an entity, are its related entities loaded too? The answer is a performance decision, and the exam wants it justified from the stated cardinalities.
The fetch policy is set with the fetch attribute of the relationship annotation:
FetchType.EAGER— load the related entities immediately.FetchType.LAZY— defer until they are actually accessed.
The defaults, which are asymmetric
When the fetch mode is not specified: a single-valued (to-one) relationship is fetched EAGERly; a collection-valued (to-many) relationship is loaded LAZYily.
Why it matters: an entity with four eager to-one relationships loads its address, department, company and profile on every single query, whether or not the application needs them — larger queries, unnecessary joins, more data transferred. The lectures recommend treating lazy as the appropriate default for all relationships, overriding to eager only where the data really is needed immediately.
Two asymmetries are worth internalising. First, in a bidirectional relationship the two directions may differ — commonly they should, since the two navigations have different cardinalities. Second, and subtler:
LAZY is a hint; EAGER is a promise
A directive to fetch lazily is only a hint to the persistence provider, which may ignore it — loading data early never breaks correctness. The converse is not true: specifying eager may be critical, because once an entity is detached the un-fetched associations can no longer be loaded at all.
The exam’s reasoning pattern is always the same. From September 2025: authors are in the thousands,
countries in the tens, books in the tens of thousands, readers in the millions. So
Reader → Country can be eager (one country per reader), while Country → Reader must be lazy (a
country has millions of readers), and Book → Author can be eager (few authors per book) while a
popular keyword’s article list must be lazy. Quote the cardinality in the justification — that is
where the marks are.
07 · Mapping
Cascading and orphan removal
By default, an entity-manager operation applies only to the entity you hand it. Sometimes that is right; sometimes it is not.
Employee emp = new Employee();
Department dept = new Department();
emp.setDepartment(dept);
em.persist(emp); // only emp is persisted — dept is not
em.remove(dept); // deletes only the Department; fails if FKs still reference itFor remove that default is usually what you want. For persist it usually is not — a dependent
child should normally be saved with its parent. The cascade attribute says so:
@OneToMany(cascade = CascadeType.PERSIST) private Address address;The available values are PERSIST, REFRESH, REMOVE, MERGE, DETACH, and ALL as shorthand for
all five. Three rules about them:
- There is no default cascade type. By default no operations are cascaded.
- Cascade settings, like relationships, are unidirectional — set them explicitly on both sides if both directions need the behaviour.
- Cascading
REMOVEcan generate inefficient SQL (related entities deleted one by one in separate statements) and should be avoided in most practical cases.
Orphan removal is a stronger, different thing, available on @OneToOne and @OneToMany:
@OneToMany(mappedBy = "dept", orphanRemoval = true) private List<Employee> employees;It suits a privately owned parent-child relationship — a weak entity, in ER terms — where each child belongs to exactly one parent through exactly one relationship. The child is deleted whenever the link is broken, not only when the parent is.
Deep dive orphanRemoval versus CascadeType.REMOVE
The two coincide when the parent is deleted and differ everywhere else. Take
dept.setEmployees(null) — simply dropping the reference:
- With orphanRemoval = true, the related
Employeeentities are removed from the database automatically, provided they are managed and no longer referenced. - With CascadeType.REMOVE, they are not removed: the cascade only fires when the parent itself is deleted.
The same distinction applies to dept.getEmployees().remove(emp) — orphan removal deletes emp,
cascade-remove does nothing. And a caveat for both: if the orphaned object is not in the managed state,
removal does not occur.
The practical rule for exam answers: use REMOVE (or orphanRemoval) only where the child genuinely
cannot exist without its parent — a configuration without its product, a profile without its author.
Never across a shared association such as authors-and-books or users-and-countries, where deleting one
side would destroy data that other entities still reference.
08 · Runtime
The entity manager, the lifecycle, and transactions
The persistence context is a kind of in-memory database of managed objects. The application never sees it directly — it talks only to the entity manager.
The EntityManager is the central authority for all persistence actions:
| Method | Effect |
|---|---|
persist(entity) | make a transient instance managed — not the same as writing it |
find(Class, primaryKey) | load by primary key; the result becomes managed, or null |
remove(entity) | schedule the tuple for deletion; the Java object still exists |
refresh(entity) | overwrite the object from the database |
flush() | write pending changes to the database as soon as possible |
Four properties of the persistence context explain the whole model: database writes happen asynchronously, at a time the provider chooses; for them to happen at all the context must be attached to a transaction; a managed entity has two lives, one as a Java object and one as a tuple bound to it, with the POJO’s id equal to the tuple’s primary key; and that binding exists only inside the persistence context — once the object leaves, it is untracked.
Hence the lifecycle:
NEW
Just constructed. Unknown to the entity manager, no persistent identity, no tuple. new Employee() alone does nothing to the database.
MANAGED
In a persistence context. Changes to the object are automatically synchronized to the database — but not vice versa.
DETACHED
Has an identity potentially matching a tuple, but changes are no longer propagated.
REMOVED
Scheduled for deletion; becomes deleted when the transaction commits or flush() runs.
persist() does not write to the database
It makes the entity managed. The row appears when the associated transaction
commits, or earlier if you call flush(). Likewise remove() does not
erase the Java object — it breaks the association and schedules the tuple for deletion. Calling
persist() on an already-managed entity is legal and triggers the cascade process.
Transactions exist at three levels: DBMS transactions, demarcated by SQL; resource-local transactions, created through the JDBC connection interface and managed by the application; and container transactions, defined through JTA and mapped by the container onto the ones below. This course uses the container level, where the entity manager is injected and the transaction is provided automatically:
@Stateless
public class MissionService {
@PersistenceContext(unitName = "MyPersistenceUnit")
private EntityManager em;
// transaction started here by the container
public void closeMission(int missionId, int reporterId) throws BadMissionForClosing {
Mission mission = em.find(Mission.class, missionId);
if (mission.getStatus() != MissionStatus.REPORTED) throw new BadMissionForClosing("…");
mission.setStatus(MissionStatus.CLOSED);
} // transaction committed by the container — no demarcation code at all
}Compare that with the JDBC version in section 2: the setAutoCommit, commit, rollback and
finally block have all disappeared. When a method calls another business method, the same
transaction is reused by default; @TransactionAttribute overrides that per method, with values
MANDATORY (a transaction must already be active), REQUIRED (the default — join one or start one),
REQUIRES_NEW (always its own; any active transaction is suspended), SUPPORTS, NOT_SUPPORTED
(suspend any active transaction) and NEVER (throw if one is active).
09 · Exam
Filling the relationship table
Since February 2025 the JPA exercise has had a fixed answer format: a table, one block per relationship direction. Practising the format is worth as much as knowing the material.
Each block asks for five things: Required? (with motivation), Owner, FetchType, CascadeType, and the complete annotation. Two instructions in the rubric catch people out: even when the answer to “Required?” is No, you must still fill in every other field as if the relationship were mapped; and you must always choose an owner, even for an N:M where either side would do.
The schema and the access patterns
Author, AuthorProfile (1:1, optional), Article (each with exactly one main author),
Keyword (N:M with Article). Scale: 50 K authors, ≤50 K profiles, 200 K articles, 2 K keywords;
1–50 articles per author; 5–10 keywords per article; popular keywords tag tens of thousands
of articles. Access patterns: (1) author → profile + articles, (2) keyword → articles + each
main author, (3) article → main author + keywords.
Author → AuthorProfile
Required? Yes — pattern 1 needs it. Owner? AuthorProfile: the author_id FK sits in its
table, and the FK decides ownership regardless of which entity the application starts from.
Fetch? EAGER — a single correlated instance. Cascade? REMOVE — a profile cannot outlive
its author. Annotation: @OneToOne(mappedBy = "author", fetch = EAGER, cascade = REMOVE).
AuthorProfile → Author
Required? No — nothing in the access patterns navigates this way; map it for symmetry. Still
answer the rest: owner AuthorProfile (unchanged — ownership is a property of the relationship,
not the direction), EAGER, no cascade, and @OneToOne @JoinColumn(name = "author_id") — the
owning side carries the join column.
Keyword → Article
Required? Yes — pattern 2. Owner? N:M, so either; pick Article and say so. Fetch?
LAZY — a popular keyword tags tens of thousands of articles. Cascade? None — N:M between
independent entities. Annotation: @ManyToMany(mappedBy = "keywords", fetch = LAZY).
Author → Article
Required? Yes — pattern 1. Owner? Article, which holds main_author_id. Fetch? LAZY
(1–50 articles; EAGER is defensible if you argue it). Cascade? REMOVE, given the schema’s
semantics. Annotation: @OneToMany(mappedBy = "mainAuthor", fetch = LAZY, cascade = REMOVE).
Then the entity code
Part (b) asks for one entity in full. For Article: @Id @GeneratedValue(IDENTITY),
@Temporal(DATE) on the date, @ManyToOne(fetch = EAGER) @JoinColumn(name = "main_author_id")
for the owner side of the author link, and the owning @ManyToMany with an explicit
@JoinTable naming article_keywords and both join columns — EAGER here, because 5–10 keywords
is a small collection.
A Category has tens of Brands; a Brand belongs to exactly one Category. Which entity owns the relationship, and where does mappedBy go?
Load-bearing ideas
- JPA removes four kinds of JDBC boilerplate: connection management,
ResultSet→object copying, object→statement copying, and manual transaction demarcation. - The four definitions. Entity = class mapped to a table. Persistence unit = all mapped classes (≈ schema). Persistence context = all managed objects (≈ instance). Managed entity = one whose changes are tracked. Asked verbatim in 2024-08-30.
- The owner is the side holding the FK — the “many” side of a 1:N, either side of a 1:1 or N:M.
@JoinColumn/@JoinTablego on the owner;mappedBygoes on the inverse; only owner-side changes are persisted; and omittingmappedBymakes JPA build a bridge table. - Fetch defaults are asymmetric: to-one EAGER, to-many LAZY. LAZY is a hint the provider may ignore; EAGER can be essential for detached objects. Justify every choice with the given cardinality.
- No cascade by default.
REMOVEonly where the child cannot exist without the parent, never across shared associations. orphanRemoval additionally deletes on link-breaking, whichCascadeType.REMOVEdoes not. persist()makes an entity managed, not written. NEW → MANAGED → DETACHED / REMOVED, with the object→database synchronization one-directional.- Container-managed transactions wrap each business method; nested calls reuse the same
transaction unless
@TransactionAttributesays otherwise. - Exam radar. Re-derive before the exam: the relationship table format with all five fields
filled even for “Required? No”; the ownership rule and what happens if you update the inverse side;
the fetch defaults; and
@OrderByplus@Lob @Basic(fetch = LAZY), the two annotations the published grader’s notes single out as commonly missing.