Querying objects, understanding state, and hooking into JPA events
Use ← → arrow keys or the buttons below to navigate.
JPQL stands for Jakarta Persistence Query Language. It lets you query your Java objects — not database tables.
-- SQL: uses table & column names
SELECT * FROM orders
WHERE customer_id = 5;
// JPQL: uses class & field names
SELECT o FROM Order o
WHERE o.customer.id = 5
SELECT, from, etc.) — but class/field names must match Java exactly// "p" is an alias for Product SELECT p FROM Product p
SELECT p FROM Product p
WHERE p.price > 100
SELECT p FROM Product p
ORDER BY p.name ASC
// Returns a list of Strings, not Products SELECT p.name FROM Product p
SELECT COUNT(p) FROM Product p
// Dot notation crosses FK relationships SELECT o FROM Order o WHERE o.customer.name = 'Alice'
Always use parameters instead of building query strings by hand — it prevents SQL injection and keeps code clean.
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();
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();
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.
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.
@Entity @NamedQuery( name = "Product.findCheap", query = "SELECT p FROM Product p " + "WHERE p.price < :max" ) public class Product { ... }
TypedQuery<Product> q = em.createNamedQuery( "Product.findCheap", Product.class ); q.setParameter("max", 20.0); List<Product> cheap = q.getResultList();
EntityName.purpose — e.g. Customer.findByEmail, Order.findPending. This makes them easy to find.
| Task | SQL | JPQL |
|---|---|---|
| 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 |
INSERT, UPDATE, or DELETE in the same way as SQL — you use the EntityManager methods (persist, merge, remove) for those.
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.
The dashed line shows that a detached entity can be re-attached with merge()
new Product(). JPA doesn't know about it yet — it's not in the database and not being tracked. Changes are invisible to JPA.
EntityManager was closed or it was explicitly detached. The object still exists in memory, but JPA won't track or save changes to it.
em.remove(entity). JPA will delete the row from the database when the transaction commits. The object still exists in Java memory until then.
em.merge(entity) to put it back into managed state first.
| Operation | Before | After | Effect |
|---|---|---|---|
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 |
JPA fires events when an entity changes state. You can attach your own Java methods to these events using callback annotations.
Note: there is no @PostRemove equivalent that's commonly needed — but it does exist in the spec.
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(); } }
Article, the timestamps are set automatically — JPA guarantees it.
@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; } }
@Transient tells JPA: "don't try to save this field to the database."
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.
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); } }
@Entity @EntityListeners( AuditListener.class ) public class Order { ... } // Apply to multiple entities: @EntityListeners({ AuditListener.class, ValidationListener.class })
| Internal Callback | External 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 |
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... }
implements Auditable and adds @EntityListeners(AuditListener.class) gets automatic timestamp tracking — no extra code needed per entity.
You have an entity class called Student with a field gpa. Which JPQL query finds all students with a GPA above 3.5?
Student (capital S), a named alias s, and the Java field gpa.Student, not the table) and the Java field name. Also, JPQL has no SELECT *.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?
em.merge() to re-attach and save.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?
getResultList() vs getSingleResult()persist, merge, remove, detach@PrePersist / @PostPersist@PreUpdate / @PostUpdate@PreRemove / @PostLoadNext week: more on relationships, fetch strategies, and the persistence context in depth.