Enterprise Computing · Week 4

JPQL, Entity Lifecycle
& Callbacks

Querying objects, understanding state, and hooking into JPA events

JPQL Queries Entity States Lifecycle Callbacks Entity Listeners

Use ← → arrow keys or the buttons below to navigate.

Part 1 — JPQL

What is JPQL?

JPQL stands for Jakarta Persistence Query Language. It lets you query your Java objects — not database tables.

SQL queries tables

-- SQL: uses table & column names
SELECT * FROM orders
WHERE customer_id = 5;

JPQL queries entities

// JPQL: uses class & field names
SELECT o FROM Order o
WHERE o.customer.id = 5
Key idea: Write your queries in terms of your Java classes. JPA translates them to the correct SQL for your database automatically.
Part 1 — JPQL

Basic JPQL Syntax

Select all entities

// "p" is an alias for Product
SELECT p FROM Product p

Filter with WHERE

SELECT p FROM Product p
WHERE p.price > 100

Order results

SELECT p FROM Product p
ORDER BY p.name ASC

Select specific fields

// Returns a list of Strings, not Products
SELECT p.name FROM Product p

Count rows

SELECT COUNT(p) FROM Product p

Navigate relationships

// Dot notation crosses FK relationships
SELECT o FROM Order o
WHERE o.customer.name = 'Alice'
Rule: Always use the entity class name and Java field names — never the table/column names from the database.
Part 1 — JPQL

Parameters & Running Queries

Always use parameters instead of building query strings by hand — it prevents SQL injection and keeps code clean.

Named parameters Preferred

String jpql = "SELECT p FROM Product p "
           + "WHERE p.price < :maxPrice";

TypedQuery<Product> q =
  em.createQuery(jpql, Product.class);

q.setParameter("maxPrice", 50.0);
List<Product> results = q.getResultList();

Positional parameters

String jpql = "SELECT p FROM Product p "
           + "WHERE p.price < ?1";

TypedQuery<Product> q =
  em.createQuery(jpql, Product.class);

q.setParameter(1, 50.0);
List<Product> results = q.getResultList();

Single result vs list

q.getResultList()

Returns a List. Safe to call even when 0 results.

q.getSingleResult()

Returns one object. Throws an exception if 0 or 2+ results found — use carefully.

Part 1 — JPQL

Named Queries

Define a query once on the entity class, then reuse it by name anywhere in your app. Cleaner than duplicating query strings all over your code.

Define on the entity

@Entity
@NamedQuery(
  name = "Product.findCheap",
  query = "SELECT p FROM Product p "
        + "WHERE p.price < :max"
)
public class Product {
  ...
}

Use it anywhere

TypedQuery<Product> q =
  em.createNamedQuery(
    "Product.findCheap",
    Product.class
  );

q.setParameter("max", 20.0);
List<Product> cheap =
  q.getResultList();
Convention: Name them EntityName.purpose — e.g. Customer.findByEmail, Order.findPending. This makes them easy to find.
Named queries are compiled and validated at application startup — you'll catch typos immediately rather than at runtime.
Part 1 — JPQL

JPQL vs SQL — Side by Side

TaskSQLJPQL
Get all rows/entities SELECT * FROM customer SELECT c FROM Customer c
Filter by field WHERE last_name = 'Lee' WHERE c.lastName = 'Lee'
Join related table JOIN orders ON ... JOIN c.orders o (uses the field)
Count rows SELECT COUNT(*) FROM ... SELECT COUNT(c) FROM Customer c
Limit results LIMIT 10 query.setMaxResults(10)
What you refer to Table names, column names Entity class names, Java field names
JPQL has no INSERT, UPDATE, or DELETE in the same way as SQL — you use the EntityManager methods (persist, merge, remove) for those.
Part 2 — Entity Lifecycle

What is the Entity Lifecycle?

Every entity object passes through a series of states during its life. The JPA runtime tracks which state each object is in and decides what to do with it.

Think of it like a document: it starts as a draft (New) → gets filed (Managed) → might be removed from the cabinet (Detached) → eventually deleted (Removed).
NEW Just created MANAGED Tracked by JPA DETACHED Exists, not tracked REMOVED Scheduled to delete persist() detach()/close() merge() remove() commit deleted from DB

The dashed line shows that a detached entity can be re-attached with merge()

Part 2 — Entity Lifecycle

The Four States Explained

NEW
You created the object with new Product(). JPA doesn't know about it yet — it's not in the database and not being tracked. Changes are invisible to JPA.
MANAGED
JPA is actively tracking this object. Any changes you make to its fields will be automatically saved to the database when the transaction commits. This is the "normal working" state.
DETACHED
The entity was once managed, but its EntityManager was closed or it was explicitly detached. The object still exists in memory, but JPA won't track or save changes to it.
REMOVED
You called em.remove(entity). JPA will delete the row from the database when the transaction commits. The object still exists in Java memory until then.
Common mistake: Modifying a detached entity and expecting it to save — it won't! You must call em.merge(entity) to put it back into managed state first.
Part 2 — Entity Lifecycle

EntityManager Operations & State Transitions

OperationBeforeAfterEffect
em.persist(e) New Managed Schedules INSERT on next flush/commit
em.find(T, id) Managed Loads entity from DB; now tracked
em.merge(e) Detached Managed (new) Returns a new managed copy; saves changes
em.remove(e) Managed Removed Schedules DELETE on next flush/commit
em.detach(e) Managed Detached Stops tracking; no more auto-save
em.refresh(e) Managed Managed Overwrites entity fields with DB values
Automatic dirty-checking: For managed entities, JPA compares the current field values against the snapshot it took when it loaded the entity. Any difference → UPDATE at commit time.
Part 3 — Callbacks & Listeners

Lifecycle Callbacks — Hook Into JPA Events

JPA fires events when an entity changes state. You can attach your own Java methods to these events using callback annotations.

Why? Business logic that must always run — e.g., setting a timestamp before saving, logging, validation — belongs here rather than scattered across your service code.

All six callback annotations

@PrePersist
Just before a new entity is saved (INSERT)
@PostPersist
After the INSERT is executed in the DB
@PreUpdate
Just before a managed entity's changes are saved
@PostUpdate
After the UPDATE is executed in the DB
@PreRemove
Just before an entity is deleted
@PostLoad
After an entity is loaded from the DB

Note: there is no @PostRemove equivalent that's commonly needed — but it does exist in the spec.

Part 3 — Callbacks & Listeners

Callback Example — Auto Timestamps

A very common use case: record when an entity was created and last modified, without relying on every developer to set those fields manually.

@Entity
public class Article {

    @Id @GeneratedValue
    private Long id;

    private String title;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;

    @PrePersist  // runs just before first INSERT
    public void onCreate() {
        createdAt = LocalDateTime.now();
        updatedAt = LocalDateTime.now();
    }

    @PreUpdate   // runs before every UPDATE
    public void onUpdate() {
        updatedAt = LocalDateTime.now();
    }
}
No matter which piece of code saves or updates an Article, the timestamps are set automatically — JPA guarantees it.
Part 3 — Callbacks & Listeners

@PostLoad — Computed Fields

@PostLoad fires after JPA loads an entity from the database. Useful for setting derived / transient fields that shouldn't be stored but need to be ready to use.

@Entity
public class Product {

    private double priceExTax;

    @Transient  // not stored in DB
    private double priceIncTax;

    @PostLoad
    public void calculateTax() {
        // 10% GST
        priceIncTax = priceExTax * 1.10;
    }
}

When is this useful?

  • Computed display values (e.g., full name from first/last)
  • Tax or currency calculations
  • Decrypting a stored encrypted field
  • Initialising transient collections or helpers
@Transient tells JPA: "don't try to save this field to the database."
Part 3 — Callbacks & Listeners

Entity Listeners — Reusable Logic

Callbacks inside the entity class work, but they mix business logic with your data class. Entity Listeners are separate classes you attach to one or more entities.

1. Create the listener class

public class AuditListener {

    @PrePersist
    public void beforeInsert(
            Object entity) {
        System.out.println(
          "Inserting: " + entity);
    }

    @PreUpdate
    public void beforeUpdate(
            Object entity) {
        System.out.println(
          "Updating: " + entity);
    }
}

2. Attach to an entity

@Entity
@EntityListeners(
    AuditListener.class
)
public class Order {
    ...
}

// Apply to multiple entities:
@EntityListeners({
    AuditListener.class,
    ValidationListener.class
})
You can attach the same listener to many entity classes — write the logic once, apply it everywhere.
Part 3 — Callbacks & Listeners

Callbacks vs Listeners — When to Use Which

Internal CallbackExternal Listener
Where defined Method inside the entity class Separate class, referenced via @EntityListeners
Good for Logic tightly coupled to this entity (e.g. its own timestamps) Cross-cutting logic shared across many entities (e.g. audit logging)
Reusability Only applies to this entity Apply same listener to any number of entities
Separation of concerns Mixed into the entity — can get messy Clean separation — entity stays focused on data
Typical uses Timestamps, derived fields, simple validation Audit trails, security checks, logging, notifications
In practice: use callbacks for simple per-entity logic, and listeners for anything you'd want to apply across multiple entities consistently.
Part 3 — Callbacks & Listeners

Putting It Together — Audit Listener

A realistic listener that records who created or updated an entity, and when:

// Shared interface for auditable entities
public interface Auditable {
    void setCreatedAt(LocalDateTime t);
    void setUpdatedAt(LocalDateTime t);
}

// The listener
public class AuditListener {

  @PrePersist
  public void onPersist(Object o) {
    if (o instanceof Auditable a) {
      LocalDateTime now =
        LocalDateTime.now();
      a.setCreatedAt(now);
      a.setUpdatedAt(now);
    }
  }

  @PreUpdate
  public void onUpdate(Object o) {
    if (o instanceof Auditable a)
      a.setUpdatedAt(
        LocalDateTime.now());
  }
}
@Entity
@EntityListeners(AuditListener.class)
public class Invoice
       implements Auditable {

  @Id @GeneratedValue
  private Long id;

  private String description;
  private LocalDateTime createdAt;
  private LocalDateTime updatedAt;

  // setters/getters...
}
Now any entity that implements Auditable and adds @EntityListeners(AuditListener.class) gets automatic timestamp tracking — no extra code needed per entity.
Check Your Understanding

Quiz — JPQL

You have an entity class called Student with a field gpa. Which JPQL query finds all students with a GPA above 3.5?

✓ Correct! JPQL uses the entity class name Student (capital S), a named alias s, and the Java field gpa.
✗ Not quite. Remember: JPQL uses the entity class name (Student, not the table) and the Java field name. Also, JPQL has no SELECT *.
Check Your Understanding

Quiz — Entity Lifecycle

A developer loads a Customer from the database, then the transaction ends and the EntityManager closes. They update the customer's email. What state is the entity in, and will the change be saved?

✓ Correct! Closing the EntityManager detaches all entities. Changes to detached entities are invisible to JPA — you must call em.merge() to re-attach and save.
✗ Not quite. When the EntityManager closes, entities become detached — they still exist in memory but JPA stops tracking them.
Check Your Understanding

Quiz — Callbacks

You want to automatically set a lastModified timestamp every time any field on an entity changes. You want this logic reusable across 10 different entity classes. What is the best approach?

✓ Correct! An EntityListener centralises the logic once and can be attached to any entity with a single annotation — exactly what they're designed for.
✗ Not quite. Duplicating logic across 10 entity classes is error-prone. An EntityListener lets you write the logic once and attach it everywhere.
Week 4 Summary

What We Covered Today

JPQL

  • Queries entities, not tables
  • Use class/field names, not SQL names
  • Named and positional parameters
  • Named queries for reuse
  • getResultList() vs getSingleResult()

Entity Lifecycle

  • New → not tracked
  • Managed → auto-saved
  • Detached → not tracked
  • Removed → pending delete
  • persist, merge, remove, detach

Callbacks & Listeners

  • @PrePersist / @PostPersist
  • @PreUpdate / @PostUpdate
  • @PreRemove / @PostLoad
  • Internal callbacks for per-entity logic
  • External listeners for shared logic
Key takeaway: JPQL keeps you working in Java-land (objects and fields). The lifecycle model means JPA handles saving for you — but you must understand the states. Callbacks and listeners let you inject business logic at exactly the right moment, cleanly.

Next week: more on relationships, fetch strategies, and the persistence context in depth.