๐ฆ JPA Queries: Concurrency and Caching
The Bank Vault Story
Imagine you run a magical bank vault. Many people want to access their treasures at the same time. But hereโs the problem: what if two people try to grab the same gold coin at once? ๐ฐ
Thatโs exactly what happens in databases! Multiple users try to read and change the same data. JPA gives us special tools to handle this safely.
๐ Entity Locking: The Guard at the Door
Think of Entity Locking like a guard at a door. The guard controls who can enter and when.
Why do we need it?
- Two people editing the same record = CHAOS!
- One personโs changes might erase anotherโs
- We need rules for who goes first
// Basic lock example
@Entity
public class BankAccount {
@Id
private Long id;
private Double balance;
}
JPA offers two types of guards:
- Optimistic - Trusting guard (checks at the end)
- Pessimistic - Strict guard (blocks everyone else)
๐ Optimistic Locking: The Trusting Approach
The Library Book Analogy
Imagine a library book everyone can read. You take it home, make notes, and return it. But when you return, the librarian checks: โDid anyone else change this book while you had it?โ
Optimistic Locking works the same way:
- Everyone can read data freely
- When you save changes, JPA checks if data changed
- If it changed โ ERROR! Try again
- If it didnโt โ SUCCESS! Your save works
How It Works: The Version Number
@Entity
public class Product {
@Id
private Long id;
private String name;
private Double price;
@Version // ๐ Magic field!
private Integer version;
}
graph TD A["User A reads Product<br>version = 1"] --> B["User B reads Product<br>version = 1"] B --> C["User B saves changes<br>version becomes 2"] A --> D["User A tries to save<br>Expected version: 1<br>Actual version: 2"] D --> E["โ OptimisticLockException!"] C --> F["โ Save successful"]
When to Use Optimistic Locking?
โ Perfect for:
- Most web applications
- When conflicts are RARE
- When you want speed
- Shopping carts, user profiles
โ Not great for:
- Bank transfers
- Ticket booking systems
- High-conflict situations
๐ Pessimistic Locking: The Strict Approach
The Hotel Room Analogy
When you book a hotel room, no one else can book it. The room is LOCKED for you until you check out.
Pessimistic Locking works the same way:
- You lock the data FIRST
- No one else can change it
- You finish your work
- You release the lock
Lock Types
// ๐ READ Lock - Others can read, but not write
entityManager.lock(account,
LockModeType.PESSIMISTIC_READ);
// ๐ WRITE Lock - Nobody else can touch it!
entityManager.lock(account,
LockModeType.PESSIMISTIC_WRITE);
Using Find with Lock
// Lock while fetching
BankAccount acc = entityManager.find(
BankAccount.class,
accountId,
LockModeType.PESSIMISTIC_WRITE
);
// Now you have exclusive access!
acc.setBalance(acc.getBalance() - 100);
Lock Mode Comparison
| Lock Type | Others Can Read? | Others Can Write? | Use Case |
|---|---|---|---|
PESSIMISTIC_READ |
โ Yes | โ No | Reports, calculations |
PESSIMISTIC_WRITE |
โ No | โ No | Money transfers |
PESSIMISTIC_FORCE_INCREMENT |
โ No | โ No | Force version bump |
โ ๏ธ Watch Out: Deadlocks!
graph TD A["User A locks Table1"] --> B["User A waits for Table2"] C["User B locks Table2"] --> D["User B waits for Table1"] B --> E["๐ DEADLOCK!<br>Both waiting forever"] D --> E
Solution: Always lock tables in the SAME ORDER!
๐พ Second-Level Cache: The Memory Shortcut
The Refrigerator Analogy
Going to the grocery store (database) every time youโre hungry is slow. But your refrigerator (cache) keeps food close by!
First-Level Cache:
- Automatic in JPA
- Lives inside EntityManager
- Gone when EntityManager closes
Second-Level Cache:
- Optional (you enable it)
- Shared across ALL EntityManagers
- Survives after EntityManager closes
Enabling Second-Level Cache
Step 1: Add Provider (EclipseLink example)
<property name="eclipselink.cache.shared.default"
value="true"/>
Step 2: Mark Entities as Cacheable
@Entity
@Cacheable // ๐ Enable caching!
public class Country {
@Id
private Long id;
private String name;
private String code;
}
Cache Modes Explained
// Always use cache
hints.put("javax.persistence.cache.retrieveMode",
CacheRetrieveMode.USE);
// Skip cache, go to database
hints.put("javax.persistence.cache.retrieveMode",
CacheRetrieveMode.BYPASS);
graph TD A["App requests Country"] --> B{In L2 Cache?} B -->|Yes| C["Return from Cache<br>โก FAST!"] B -->|No| D["Query Database<br>๐ข Slower"] D --> E["Store in L2 Cache"] E --> F["Return to App"]
When to Cache?
| Cache This โ | Donโt Cache This โ |
|---|---|
| Country list | User sessions |
| Product categories | Order details |
| Static settings | Real-time prices |
| Rarely changed data | Frequently changed data |
๐ณ Entity Graphs: The Smart Shopper
The Shopping List Analogy
Imagine going to a store. Without a list, you might:
- Forget items (N+1 problem)
- Buy too much (over-fetching)
Entity Graphs are your shopping list! They tell JPA exactly what to load.
The N+1 Problem
// ๐ฑ BAD: N+1 queries!
List<Order> orders = em.createQuery(
"SELECT o FROM Order o").getResultList();
for (Order o : orders) {
// Each call = 1 more query!
System.out.println(o.getCustomer().getName());
}
// 1 query for orders + N queries for customers = N+1
Entity Graph Solution
Define the Graph:
@Entity
@NamedEntityGraph(
name = "Order.withCustomer",
attributeNodes = @NamedAttributeNode("customer")
)
public class Order {
@Id
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
private Customer customer;
private Double total;
}
Use the Graph:
EntityGraph graph = em.getEntityGraph(
"Order.withCustomer");
Map<String, Object> hints = new HashMap<>();
hints.put("javax.persistence.fetchgraph", graph);
// ๐ฏ ONE query loads everything!
List<Order> orders = em.createQuery(
"SELECT o FROM Order o")
.setHint("javax.persistence.fetchgraph", graph)
.getResultList();
Dynamic Entity Graphs
// Create graph on the fly
EntityGraph<Order> graph =
em.createEntityGraph(Order.class);
graph.addAttributeNodes("customer");
graph.addAttributeNodes("items");
// Now use it!
Fetch Graph vs Load Graph
| Type | Behavior |
|---|---|
fetchgraph |
Load ONLY whatโs in graph |
loadgraph |
Load graph + default eager fields |
๐ Callbacks and Listeners: The Event Watchers
The Security Camera Analogy
Imagine security cameras in your bank. They donโt stop anything, but they WATCH and RECORD everything.
Callbacks are like cameras. They watch entity events:
- When created
- When updated
- When deleted
- When loaded
Lifecycle Events
graph TD A["Entity Created"] --> B["@PrePersist"] B --> C["Saved to DB"] C --> D["@PostPersist"] E["Entity Loaded"] --> F["@PostLoad"] G["Entity Updated"] --> H["@PreUpdate"] H --> I["Saved to DB"] I --> J["@PostUpdate"] K["Entity Deleted"] --> L["@PreRemove"] L --> M["Removed from DB"] M --> N["@PostRemove"]
Using Callbacks in Entity
@Entity
public class AuditedEntity {
@Id
private Long id;
private String data;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@PrePersist
public void beforeSave() {
this.createdAt = LocalDateTime.now();
System.out.println("About to save!");
}
@PostPersist
public void afterSave() {
System.out.println("Saved with ID: " + id);
}
@PreUpdate
public void beforeUpdate() {
this.updatedAt = LocalDateTime.now();
}
@PostLoad
public void afterLoad() {
System.out.println("Entity loaded!");
}
}
External Listeners (Cleaner Code!)
Step 1: Create Listener Class
public class AuditListener {
@PrePersist
public void setCreatedAt(Object entity) {
if (entity instanceof Auditable) {
((Auditable) entity)
.setCreatedAt(LocalDateTime.now());
}
}
@PreUpdate
public void setUpdatedAt(Object entity) {
if (entity instanceof Auditable) {
((Auditable) entity)
.setUpdatedAt(LocalDateTime.now());
}
}
}
Step 2: Attach to Entity
@Entity
@EntityListeners(AuditListener.class)
public class Product implements Auditable {
@Id
private Long id;
private String name;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
// getters and setters...
}
Callback Summary
| Callback | When It Fires | Common Use |
|---|---|---|
@PrePersist |
Before INSERT | Set creation time |
@PostPersist |
After INSERT | Log creation |
@PreUpdate |
Before UPDATE | Set modified time |
@PostUpdate |
After UPDATE | Trigger notifications |
@PreRemove |
Before DELETE | Validate deletion |
@PostRemove |
After DELETE | Cleanup related data |
@PostLoad |
After SELECT | Calculate derived fields |
๐ฏ Quick Decision Guide
graph LR A["Need Data Protection?"] --> B{How often conflicts?} B -->|Rarely| C["Use Optimistic Locking<br>@Version annotation"] B -->|Often| D["Use Pessimistic Locking<br>LockModeType"] E["Slow Queries?"] --> F{Data changes often?} F -->|No| G["Enable Second-Level Cache<br>@Cacheable"] F -->|Yes| H["Use Entity Graphs<br>@NamedEntityGraph"] I["Need Automatic Actions?"] --> J["Use Callbacks<br>@PrePersist, @PostUpdate..."]
๐ Key Takeaways
- Entity Locking = Guard at the door (prevents conflicts)
- Optimistic = Trust, then verify (uses @Version)
- Pessimistic = Lock first, ask later (blocks access)
- Second-Level Cache = Refrigerator (faster data access)
- Entity Graphs = Shopping list (load exactly what you need)
- Callbacks = Security cameras (watch all events)
Youโre now ready to handle concurrent access like a pro! ๐
