spring-bootcachingspring-data-jpahazelcast

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 :-)


Solution

  • 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.