Concurrency and Caching

Back

Loading concept...

๐Ÿฆ 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:

  1. Optimistic - Trusting guard (checks at the end)
  2. 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&lt;br&gt;โšก FAST!"] B -->|No| D["Query Database&lt;br&gt;๐Ÿข 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&lt;br&gt;@Version annotation"] B -->|Often| D["Use Pessimistic Locking&lt;br&gt;LockModeType"] E["Slow Queries?"] --> F{Data changes often?} F -->|No| G["Enable Second-Level Cache&lt;br&gt;@Cacheable"] F -->|Yes| H["Use Entity Graphs&lt;br&gt;@NamedEntityGraph"] I["Need Automatic Actions?"] --> J["Use Callbacks&lt;br&gt;@PrePersist, @PostUpdate..."]

๐Ÿ† Key Takeaways

  1. Entity Locking = Guard at the door (prevents conflicts)
  2. Optimistic = Trust, then verify (uses @Version)
  3. Pessimistic = Lock first, ask later (blocks access)
  4. Second-Level Cache = Refrigerator (faster data access)
  5. Entity Graphs = Shopping list (load exactly what you need)
  6. Callbacks = Security cameras (watch all events)

Youโ€™re now ready to handle concurrent access like a pro! ๐Ÿš€

Loading story...

Story - Premium Content

Please sign in to view this story and start learning.

Upgrade to Premium to unlock full access to all stories.

Stay Tuned!

Story is coming soon.

Story Preview

Story - Premium Content

Please sign in to view this concept and start learning.

Upgrade to Premium to unlock full access to all content.