j3d
j3d

Reputation: 9734

How to cache the view result of a Spring JPA query

Given the following entity...

@Entity
@Getter
@Setter
public class OrderDTD {

    private Long id;
    ...
    
    private String orderType;
    private String orderStatus;
    

... and the following view...

public interface OrderStatsView {

  /** Gets the order type. */
  String getOrderType();

  /** Gets the total number of orders. */
  Long getOrderCount();

  /** Gets the number of executed orders. */
  Long getExecutedOrderCount();
}

... I've created a method to retrieve some stats for a given order:

@Repository
public interface OrderRepository
    extends JpaRepository<OrderDTD, Long> {

    ...

    @Query("SELECT o.orderType AS orderType,"
        + " COUNT(o.orderStatus) AS orderCount,"
        + " COUNT(CASE WHEN o.orderStatus = 'EXECUTED'"
        + " THEN 1 END) AS executedOrderCount"
        + " FROM OrderDTD o"
        + " WHERE o.orderType = :orderType"
        + " GROUP BY o.orderType")
    @Cacheable("orderStatsView")
    Optional<OrderStatsView> getOrderStats(@NonNull String orderType);

    @Override
    @CacheEvict(value = "orderStatsView", key = "#p0.orderType")
    <T extends OrderDTD> T save(T order);
}

Method getOrderStats() works as expected without caching... whereas when I enable caching with @Cacheable, I always get this error:

com.hazelcast.nio.serialization.HazelcastSerializationException: Failed to serialize 'jdk.proxy3.$Proxy584'

Am I missing something? Any help would be really appreciated :-)

Upvotes: 0

Views: 154

Answers (2)

M. Deinum
M. Deinum

Reputation: 125202

The problem is your projection.

public interface OrderStatsView {

  /** Gets the order type. */
  String getOrderType();

  /** Gets the total number of orders. */
  Long getOrderCount();

  /** Gets the number of executed orders. */
  Long getExecutedOrderCount();
}

Your projection is an interface as an interface has no implementation Spring Data will generate one for you based on the result. It will do this dynamically, that proxy isn't serializable. You don't want that to be serializable else all underlying things from Hibernate (session etc.) would be serialized as well.

Next you probably configured Hazelcast with some defaults which will try to serialize using Java serialzation (which isn't really recommended). But that as an aside.

What you can do is instead of using interfaces uses classes. In your case a record would work.

public record OrderStatsView(String orderType, long orderCount, long executedOrderCount) implements Serializable {};

Now this will be mapped directly to the type instead of a proxy and is made Serializable as well.

I would however (as mentioned as an aside) to also re-configure your Hazelcast to use some other serialization mechanism, like JSON to store the objects.

Query Caching

Another option would be to leave the interfaces as is and use query caching and configure Hibernate to use Hazelcast for this caching. Which will do a lot of caching (and eviction) for you and will probably save you some caching headaches.

Upvotes: 1

John Williams
John Williams

Reputation: 5430

Add the annotation to your service layer, to the methods that call the Spring Data repositories, eg

@Cacheable("orderStatsView")
    public Optional<OrderStatsView> getOrderStats(@NonNull String orderType) {
    return orderRepository.getOrderStats();
}

@CacheEvict(value = "orderStatsView", key = "#p0.orderType")
<T extends OrderDTD> T save(T order) {
    orderRepository.save(order);
}

Upvotes: -1

Related Questions