web-dev-qa-db-fra.com

Comment mettre en cache les résultats d'une méthode de requête Spring Data JPA sans utiliser le cache de requête?

J'ai une application Spring Boot avec des classes de référentiel Spring Data JPA (hibernate backend). J'ai ajouté quelques méthodes personnalisées du Finder, certaines avec des @Query annotation pour lui indiquer comment obtenir les données. J'ai déjà configuré EhCache pour le cache de niveau 2 en veille prolongée, mais jusqu'à présent, la seule façon d'obtenir ces résultats de mise en cache est d'activer le cache de requête en veille prolongée. Je préfère définir un cache spécifique et y stocker les objets de domaine réels comme s'il s'agissait d'un Finder normal. Voici mon code de mise en pension:

public interface PromotionServiceXrefRepository extends PagingAndSortingRepository<PromotionServiceXref, Integer> {

  @Query("SELECT psx FROM Customer c " +
         "JOIN c.customerProductPromotions cpp " +
         "JOIN cpp.productPromotion pp " +
         "JOIN pp.promotion p JOIN p.promotionServiceXrefs psx " +
         "WHERE c.customerId = ?1")
  @QueryHints(@QueryHint(name = "org.hibernate.cacheable", value = "true"))
  @Cache(usage = CacheConcurrencyStrategy.READ_ONLY, region = "promotionServiceXrefByCustomerId")
  Set<PromotionServiceXref> findByCustomerId(int customerId);
}

Et voici le cache "promotionServiceXrefByCustomerId" que j'ai défini, qui n'est PAS utilisé:

<cache name="promotionServiceXrefByCustomerId" overflowToDisk="true" diskPersistent="true"
       maxEntriesLocalHeap="3000000" eternal="true" diskSpoolBufferSizeMB="20" memoryStoreEvictionPolicy="LFU"
       transactionalMode="off" statistics="true">
</cache>

Qu'est-ce que je fais mal? Si j'active StandardQueryCache, ces données y sont mises en cache et hibernate n'exécute pas de requête. Mais lorsque je désactive la mise en cache des requêtes, cela n'est pas mis en cache. Qu'est-ce que je fais mal ici? SVP AIDEZ!

18
Kevin M

La raison pour laquelle le code que vous avez ne fonctionne pas est que @Cache N'est pas destiné à fonctionner de cette façon. Si vous souhaitez mettre en cache les résultats d'une exécution de méthode de requête, le moyen le plus simple consiste à utiliser mise en cache de l'abstraction de Spring.

interface PromotionServiceXrefRepository extends PagingAndSortingRepository<PromotionServiceXref, Integer> {

  @Query("…")
  @Cacheable("servicesByCustomerId")
  Set<PromotionServiceXref> findByCustomerId(int customerId);

  @Override
  @CacheEvict(value = "servicesByCustomerId", key = "#p0.customer.id")
  <S extends PromotionServiceXref> S save(S service);
}

Cette configuration entraînera la mise en cache des résultats des appels à findByCustomerId(…) par l'identifiant client. Notez que nous avons ajouté un @CacheEvict À la méthode save(…) remplacée, afin que le cache que nous remplissons avec la méthode de requête soit supprimé, chaque fois qu'une entité est enregistrée. Cela doit probablement aussi être propagé aux méthodes delete(…).

Vous pouvez maintenant aller de l'avant et configurer un CacheManager dédié (voir la documentation de référence pour plus de détails) pour brancher la solution de mise en cache que vous préférez (en utilisant un ConcurrentHashMap ici) .

 @Configuration
 @EnableCaching
 class CachingConfig {

   @Bean
   CacheManager cacheManager() {

     SimpleCacheManager cacheManager = new SimpleCacheManager();
     cacheManager.addCaches(Arrays.asList(new ConcurrentMapCache("servicesByCustomerId)));

     return cacheManager;
   }
 }
53
Oliver Drotbohm

Vous devez être conscient qu'en abandonnant le Hibernate QueryCache, vous êtes responsable d'invalider les requêtes qui deviennent obsolètes lors de l'enregistrement, de la mise à jour, de la suppression des entités qui ont influencé le résultat de la requête (ce que fait Oliver en définissant CacheEvict lors de l'enregistrement) - ce que je pense peut être une douleur - ou du moins vous devez en tenir compte et l'ignorer si ce n'est pas vraiment un problème pour votre scénario.

13
Balamaci Serban

Je cite d'abord votre question:

Qu'est-ce que je fais mal?

La façon dont vous essayez de nom le cache est pas approprié comment hibernate l'utilisera. Cochez org.hibernate.engine.spi.CacheInitiator Qui utilise org.hibernate.internal.CacheImpl Qui est basé sur:

if ( settings.isQueryCacheEnabled() ) {
    final TimestampsRegion timestampsRegion = regionFactory.buildTimestampsRegion(
            qualifyRegionName( UpdateTimestampsCache.REGION_NAME ),
            sessionFactory.getProperties()
    );
    updateTimestampsCache = new UpdateTimestampsCache( sessionFactory, timestampsRegion );
    ...
}

Et UpdateTimestampsCache.REGION_NAME (Égal à org.hibernate.cache.spi.UpdateTimestampsCache) Est ce qui vous manque en tant que nom du cache. Pour le cache de requête vous devrez utiliser exactement ce nom de cache et pas d'autre!

Maintenant, quelques autres réflexions liées à votre problème:

  • la suppression de @Cache et la définition du nom du cache sur org.hibernate.cache.spi.UpdateTimestampsCache permettra à votre requête d'être mise en cache avec ehcache par hibernate (l'abstraction du cache de printemps n'est pas impliquée ici)
  • définir un nom de cache codé en dur ne vous rendra pas heureux, je suis sûr, mais au moins vous savez pourquoi cela se produit
  • Balamaci Serban (le post juste en dessous) a douloureusement raison

Ci-dessous la configuration d'un de mes projets où ehcache + @Query + @QueryHints fonctionne comme prévu (fichier ehcache/ehcache-in-memory.xml):

<?xml version="1.0" encoding="UTF-8"?>
<ehcache name="in-memory" xmlns="http://ehcache.org/ehcache.xsd">
    <!--<diskStore path="Java.io.tmpdir"/>-->

    <!--
        30d = 3600×24×30 = 2592000
    -->

    <cache name="org.hibernate.cache.internal.StandardQueryCache"
           maxElementsInMemory="9999" eternal="false"
           timeToIdleSeconds="2592000" timeToLiveSeconds="2592000"
           overflowToDisk="false" overflowToOffHeap="false"/>

    <cache name="org.hibernate.cache.spi.UpdateTimestampsCache"
           maxElementsInMemory="9999" eternal="true"
           overflowToDisk="false" overflowToOffHeap="false"/>

    <defaultCache maxElementsInMemory="9999" eternal="false"
                  timeToIdleSeconds="2592000" timeToLiveSeconds="2592000"
                  overflowToDisk="false" overflowToOffHeap="false"/>
</ehcache>

et hibernate.properties:

hibernate.jdbc.batch_size=20
hibernate.show_sql=true
hibernate.format_sql=true
hibernate.validator.autoregister_listeners=false
hibernate.cache.use_second_level_cache=true
hibernate.cache.use_query_cache=true
hibernate.cache.region.factory_class=org.hibernate.cache.ehcache.EhCacheRegionFactory
hibernate.hbm2ddl.auto=update
net.sf.ehcache.configurationResourceName=ehcache/ehcache-in-memory.xml
hibernate.dialect=org.hibernate.dialect.H2Dialect

et quelques versions de pom.xml pour lesquelles mon explication s'applique:

<springframework.version>5.0.6.RELEASE</springframework.version>
<spring-security.version>5.0.5.RELEASE</spring-security.version>
<spring-data-jpa.version>2.1.0.RELEASE</spring-data-jpa.version>
<hibernate.version>5.2.13.Final</hibernate.version>
<jackson-datatype-hibernate5.version>2.9.4</jackson-datatype-hibernate5.version>

Et le test de fonctionnement complet est image.persistence.repositories.ImageRepositoryTest.Java trouvé ici: https://github.com/adrhc/photos-server/tree/how-to -cache-results-of-a-spring-data-jpa-query-method-without-using-query-cache
Oui, exécutez mvn clean install Ou changez mon env.sh Si vous voulez vraiment utiliser mes scripts Shell. Vérifiez ensuite le nombre de requêtes SQL au nom de l'appel 3x imageRepository.count().

3
adrhc