1. Introduction

Java Persistence API (JPA) acts as a bridge between Java objects and relational databases, allowing us to persist and retrieve data seamlessly. In this tutorial, we’ll explore various strategies and techniques to effectively refresh and fetch entities after saving operations in JPA.

2. Understanding Entity Management in Spring Data JPA

In Spring Data JPA, entity management revolves around the JpaRepository interface, which serves as the primary mechanism for interacting with the database. Through the JpaRepository interface, which extends CrudRepository, Spring Data JPA offers a robust set of methods for entity persistence, retrieval, updating, and deletion.

Furthermore, the entityManager is automatically injected into these repository interfaces by the Spring container. This component is an integral part of the JPA infrastructure embedded within Spring Data JPA, facilitating interaction with the underlying persistence context and the execution of JPA queries.

2.1. Persistence Context

One crucial component within JPA is the persistence context. Imagine this context as a temporary holding area where JPA manages the state of retrieved or created entities.

It ensures that:

  • Entities are unique: Only one instance of an entity with a specific primary key exists within the context at any given time.
  • Changes are tracked: The EntityManager keeps track of any modifications made to entity properties within the context.
  • Data consistency is maintained: The EntityManager synchronizes changes made within the context with the underlying database during transactions.

2.2. Lifecycle of JPA Entities

There are four distinct lifecycle stages of JPA entities: New, Managed, Removed, and Detached.

When we create a new entity instance using the entity’s constructor, it is in the “New” state. We can verify this by checking if the entity’s ID (primary key) is null:

Order order = new Order();
if (order.getId() == null) {
    // Entity is in the "New" state
}

After we persist the entity using a repository’s save() method, it transitions to the “Managed” state. We can verify this by checking if the saved entity exists in the repository:

Order savedOrder = repository.save(order);
if (repository.findById(savedOrder.getId()).isPresent()) {
    // Entity is in the "Managed" state
}

When we call the repository’s delete() method on a managed entity, it transitions to the “Removed” state. We can verify this by checking if the entity is no longer present in the database after deletion:

repository.delete(savedOrder);
if (!repository.findById(savedOrder.getId()).isPresent()) {
    // Entity is in the "Removed" state
}

Lastly, once the entity is detached using the repository’s detach() method, the entity is no longer associated with the persistence context. Changes made to a detached entity won’t be reflected in the database unless explicitly merged back into the managed state. We can verify this by attempting to modify the entity after detaching it:

repository.detach(savedOrder);
// Modify the entity
savedOrder.setName("New Order Name");

If we call save() on a detached entity, it re-attaches the entity to the persistence context and persists the changes to the database upon flushing the persistence context.

3. Saving Entities with Spring Data JPA

When we invoke save(), Spring Data JPA schedules the entity for insertion into the database upon transaction commit. It adds the entity to the persistence context, marking it as managed.

Here’s a simple code snippet demonstrating how to use the save() method in Spring Data JPA to persist an entity:

However, it’s important to note that invoking save() doesn’t immediately trigger a database insert operation. Instead, it merely transitions the entity to the managed state within the persistence context. Thus, if other transactions read data from the database before our transaction commits, they might retrieve outdated data that doesn’t include the changes we’ve made but not yet committed.

To ensure that the data remains up-to-date, we can employ two approaches: fetching and refreshing.

4. Fetching Entities in Spring Data JPA

When we fetch an entity, we don’t discard any modifications made to it within the persistence context. Instead, we simply retrieve the entity’s data from the database and add it to the persistence context for further processing.

4.1. Using findById()

Spring Data JPA repositories offer convenient methods like findById() for retrieving entities. These methods always fetch the latest data from the database, regardless of the entity’s state within the persistence context. This approach simplifies entity retrieval and eliminates the need to manage the persistence context directly.

Order order = repository.findById(1L).get();

4.2. Eager vs. Lazy Fetching

In eager fetching, all related entities associated with the main entity are retrieved from the database at the same time as the main entity. By setting fetch = FetchType.EAGER on the orderItems collection, we instruct JPA to eagerly fetch all associated OrderItem entities when retrieving an Order:

@Entity
public class Order {
    @Id
    private Long id;

    @OneToMany(mappedBy = "order", fetch = FetchType.EAGER)
    private List<OrderItem> orderItems;
}

This means that after the findById() call, we can directly access the orderItems list within the order object and iterate through the associated OrderItem entities without requiring any additional database queries:

Order order = repository.findById(1L).get();

// Accessing OrderItems directly after fetching the Order
if (order != null) {
    for (OrderItem item : order.getOrderItems()) {
        System.out.println("Order Item: " + item.getName() + ", Quantity: " + item.getQuantity());
    }
}

On the other hand, by setting fetch = FetchType.LAZY, related entities will not retrieved from the database until they are explicitly accessed within the code:

@Entity
public class Order {
    @Id
    private Long id;

    @OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
    private List<OrderItem> orderItems;
}

*When we call order.getOrderItems(), a separate database query is executed to fetch the associated OrderItem entities for that order.* This additional query is only triggered because we explicitly accessed the orderItems list:

Order order = repository.findById(1L).get();

if (order != null) {
    List<OrderItem> items = order.getOrderItems(); // This triggers a separate query to fetch OrderItems
    for (OrderItem item : items) {
        System.out.println("Order Item: " + item.getName() + ", Quantity: " + item.getQuantity());
    }
}

4.3. Fetching with JPQL

Java Persistence Query Language (JPQL) allows us to write SQL-like queries that target entities instead of tables. It provides flexibility for retrieving specific data or entities based on various criteria.

Let’s see an example of fetching orders by customer name and with the order date falling within a specified range:

@Query("SELECT o FROM Order o WHERE o.customerName = :customerName AND 
  o.orderDate BETWEEN :startDate AND :endDate")
List<Order> findOrdersByCustomerAndDateRange(@Param("customerName") String customerName, 
  @Param("startDate") LocalDate startDate, @Param("endDate") LocalDate endDate);

4.4. Fetching with Criteria API

The Criteria API in Spring Data JPA provides a reliable and flexible method to create queries dynamically. It allows us to construct complex queries safely using method chaining and criteria expressions, ensuring that our queries are error-free at compile time.

Let’s consider an example where we use the Criteria API to fetch orders based on a combination of criteria, such as customer name and order date range:

CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Order> criteriaQuery = criteriaBuilder.createQuery(Order.class);
Root<Order> root = criteriaQuery.from(Order.class);

Predicate customerPredicate = criteriaBuilder.equal(root.get("customerName"), customerName);
Predicate dateRangePredicate = criteriaBuilder.between(root.get("orderDate"), startDate, endDate);

criteriaQuery.where(customerPredicate, dateRangePredicate);

return entityManager.createQuery(criteriaQuery).getResultList();

5. Refreshing Entities with Spring Data JPA

Refreshing entities in JPA ensures that the in-memory representation of entities in the application stays synchronized with the latest data stored in the database. When other transactions modify or update entities, the data within the persistence context might become outdated. Refreshing entities allow us to retrieve the most recent data from the database, preventing inconsistencies and maintaining data accuracy.

5.1. Using refresh()

In JPA, we achieve entity refreshing using the refresh() method provided by the EntityManager. Invoking refresh() on a managed entity discards any modifications made to the entity within the persistence context. It reloads the entity’s state from the database, effectively replacing any modifications made since the entity was last synchronized with the database.

However, it’s important to note that Spring Data JPA repositories do not offer a built-in refresh() method.

Here’s how to refresh an entity using the EntityManager:

@Autowired
private EntityManager entityManager;

entityManager.refresh(order);

5.2. Handling OptimisticLockException

The @Version annotation in Spring Data JPA serves the purpose of implementing optimistic locking. It helps ensure data consistency when multiple transactions might try to update the same entity concurrently. When we use @Version, JPA automatically creates a special field (often named version) on our entity class.

This field stores an integer value representing the version of the entity in the database:

@Entity
public class Order {
    @Id
    @GeneratedValue
    private Long id;
    
    @Version
    private Long version;
}

When retrieving an entity from the database, JPA actively fetches its version. Upon updating the entity, JPA compares the version of the entity in the persistence context with the version stored in the database. If the versions of the entity differ, it indicates that another transaction has modified the entity, potentially leading to data inconsistency.

In such cases, JPA throws an exception, often an OptimisticLockException, to indicate the potential conflict. Therefore, we can call the refresh() method within the catch block to reload the entity’s state from the database.

Let’s see a brief demonstration of how this approach works:

Order order = orderRepository.findById(orderId)
  .map(existingOrder -> {
      existingOrder.setName(newName);
      return existingOrder;
  })
  .orElseGet(() -> {
      return null;
  });

if (order != null) {
    try {
        orderRepository.save(order);
    } catch (OptimisticLockException e) {
        // Refresh the entity and potentially retry the update
        entityManager.refresh(order);
        // Consider adding logic to handle retries or notify the user about the conflict
    }
}

Additionally, it’s worth noting that refresh() may throw javax.persistence.EntityNotFoundException if the entity being refreshed has been removed from the database by another transaction since it was last retrieved.

6. Conclusion

In this article, we learned the distinction between refreshing and fetching entities in Spring Data JPA. Fetching involves retrieving the latest data from the database when needed. Refreshing involves updating the entity’s state in the persistence context with the most recent data from the database.

By utilizing these approaches strategically, we can maintain data consistency and ensure that all transactions operate on the latest data.

As always, the source code for the examples is available over on GitHub.