在JpaRepository(Spring Data)中对方法进行缓存

9

工具: Spring-Boot:1.5.9.RELEASE Spring-Data-JPA:1.11.9.RELEASE

问题: 目前我有一个扩展自JpaRepository的存储库。为了避免频繁访问数据库,我想在JpaRepository中缓存一些CRUD方法。 我试过了一些方式,从谷歌先生那里找到了一些,但除了其中一种方法外,其他都不起作用。

已编辑 1. 在link中提到的解决方案是可行的。然而,在这里存在一个不良实践(对我来说是冗余)。想象一下,如果我有50个扩展JpaRepository的存储库,这意味着我必须在50个存储库中覆盖保存方法。

     public interface UserRepository extends CrudRepository<User, Long> {
        @Override
        @CacheEvict("user")
        <S extends User> S save(S entity);

        @Cacheable("user")
        User findByUsername(String username);
     }

编辑 2. 扩展JpaRepository接口。我在link2看到了一些可能有效的东西。

链接中提到了3种不同的方式来缓存JpaRepository方法。第一种方法与我在#1中提到的相同。然而,我想要类似于第二/三种方法的东西,这样我就不需要在所有存储库中重复覆盖CRUD方法

以下是我编写的一些示例代码。

    @NoRepositoryBean        
    public interface BaseRepository<T, ID extends Serializable> extends 
    JpaRepository<T, ID> {

        @CacheEvict
        <S extends User> S save(S entity);

        @Cacheble
        T findOne(ID id);
    }

    @Repository
    @CacheConfig("user")
    public interface UserRepository extends BaseRepository<User, Integer> {
        // when I calling findOne/save method from UserRepository, it should 
        // caching the methods based on the CacheConfig name defined in the 
        // child class.
    }

然而,似乎上面的代码无法正常工作,因为我收到了以下异常。我理解问题主要是因为在BaseRepository中没有为可缓存注释分配名称所致。但是,我需要缓存从JpaRepository扩展的BaseRepository中的CRUD方法。
java.lang.IllegalStateException: 未使用解析器'org.springframework.cache.interceptor.SimpleCacheResolver@30a9fd0'解析“Builder [public abstract java.util.List com.sdsap.app.repository.BaseRepository.findAll()] caches = [] | key ='' | keyGenerator ='' | cacheManager ='' | cacheResolver ='' | condition ='' | unless ='' | sync ='false'”的任何缓存。每个缓存操作应提供至少一个缓存。
我已经向谷歌先生询问了几天,但仍然找不到合适的解决方案。希望有人能在这里帮助我。如果我的问题不清楚或遗漏了某些内容,请见谅,因为这是我第一次在这里发布。谢谢!

不要这么说。假设你正在使用JPA(假设是Hibernate),它已经支持缓存,并且在这方面将比Spring Cache更好地集成。因此,只需以正确的方式配置ORM,使第二级缓存已正确配置和启用即可。 - M. Deinum
@M.Deinum,是的,我现在正在使用Hibernate。我还在使用Spring Redis作为我的缓存层。我不确定是否容易切换到Hibernate缓存。如果您能提供一些文档/链接作为指导,那将是非常好的。谢谢! - xiaoli
我从未说过你需要交换缓存,我说过你应该为ORM使用缓存集成。不确定是否有适用于Hibernate的Redis缓存提供程序。实际上,您可以配置Hibernate使用任何JCache实现,而且Redis也有一个,所以这应该不难。 - M. Deinum
我看到了一些有关Hibernate与Redis集成的示例。我会查看这些文章。感谢您的帮助。 - xiaoli
3个回答

2

我假设您已经设置好了所需的配置,并且您发布的堆栈跟踪是问题所在。那么让我们来解决它。

我看到两个问题:

  1. java.lang.IllegalStateException: No cache could be resolved, At least one cache should be provided per cache operation

    解决方案:每当您想要缓存数据或清除数据时,您必须提供缓存的名称,但我没有在您的代码中看到提供缓存名称。

    应该定义@Cacheable的cacheNames或value以使缓存工作。

    示例@Cacheable(value = "usersCache")

  2. 正确的缓存键

    因为缓存工作在key-value对上,所以您应该提供一个合适的缓存键。如果您不提供缓存键,则默认情况下会创建一个默认的键生成策略,该策略创建一个SimpleKey,其中包含调用该方法的所有参数。

建议:您应该手动提供缓存键。

示例:

@Cacheable(value = "usersCache", key = "#username")
User findByUsername(String username);

注意:确保用户名唯一,因为缓存键必须唯一。

您可以阅读更多Spring缓存注释:一些技巧和窍门


谢谢回复。我已经完成了所有的配置设置,应用程序也运行良好。我只剩下缓存问题。我知道必须提供一个带有名称的可缓存注释。然而,目前我有一个扩展jparepository的baserepository。由于它将作为所有repository的基础使用,我无法在baserepository中放置特定的cacheable名称。 - xiaoli
这个回答与所问问题完全无关。问题基本上是,Spring CrudRepository 实现如何从 UserRepository 中定义的 @CacheConfig 注解获取缓存的名称? - Bob 1174

0

在你想要缓存的方法上使用@CachedResult

在你的主类中使用@EnableCaching

示例代码:Main

@SpringBootApplication
@EnableCaching
@RestController
public class SpringBootCacheApplication {

    @Autowired
    SomeBean someBean;

    @RequestMapping(value = "/cached/{key}")
    public int getCachedMethod(@PathVariable("key") String key) {
        System.out.println("Got key as " + key);
        return someBean.someCachedResult(key);
    }

    public static void main(String[] args) {
        SpringApplication.run(SpringBootCacheApplication.class, args);
    }
}

SomeBean类包含我希望缓存的方法

@Component
public class SomeBean {

    @CacheResult
    public int someCachedResult(String key) {
        System.out.println("Generating random number");
        int num = new Random().nextInt(200);
        return num;
    }

}

someCachedResult方法中,我总是返回一些随机值。由于它被缓存了,你只会在第一次获取时得到一个随机值。

这里的SomeBean应该对应于你的CachingUserRepository类。


谢谢回复。我以前没有使用过CacheResult。这很有趣。但我的主要问题主要是如何在扩展自jparepository的baserepository中缓存基本的CRUD方法。 - xiaoli
你可以实现你想要的crud方法,并在实现中调用super.method,然后在它们上面加上@CacheResult注解。 - Shanu Gupta
我之前已经实现了crud方法。但是,当我尝试在BaseRepositoryImpl中实现它们时,缓存仍然无法工作(相同的错误-找不到缓存)。我还没有尝试使用CacheResult,但我会尝试一下。感谢您的帮助!!! - xiaoli

0

这是一个很好的想法。我最终尝试了一下并使其工作。

我创建了一个 BaseRepository

@NoRepositoryBean
public interface BaseRepository<T, ID extends Serializable> extends JpaRepository<T, ID> {
    @Cacheable(cacheResolver = "cachingConfig")
    Optional<T> findById(UUID id);
    
    @CachePut(cacheResolver = "cachingConfig", key = "#p0.id")
    // Worth noting - add multiple cache puts if caching by different keys(queries)
    // This gets hard when caching special queries per resource - best I've
    //   found so far is to override this method in resource repositories and add all
    //   the puts/evicts needed
    <S extends T> S save(S entity);
}

请注意 cacheResolve = "cachingConfig"。 然后使用 CachingConfigCacheResolver)来解决您的缓存异常问题:
@Configuration
@EnableCaching
@Log4j2
public class CachingConfig implements CacheResolver {
    private final CacheManager cacheManager;
    private final ObjectMapper objectMapper;

    public CachingConfig(ObjectMapper objectMapper) {
        this.cacheManager = new ConcurrentMapCacheManager();
        this.objectMapper = objectMapper;
    }

    @Bean
    public CacheManager cacheManager() {
        return cacheManager;
    }

    @Override
    public Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context) {
        Collection<Cache> caches = new ArrayList<>();

        String cacheName = (context.getTarget() instanceof BaseRepository)
                // When BaseRepository, first interface in list is specific Repository Interface
                ? context.getTarget().getClass().getInterfaces()[0].getSimpleName()
                    // I've standardized around all uppercase domain (UserRepository = USER)
                    .replace("Repository", "").toUpperCase(Locale.ROOT)
                // Fallback to class name (you may have different ideas here)
                : context.getTarget().getClass().getSimpleName();

        caches.add(cacheManager.getCache(cacheName));

        return caches;
    }

    // Periodic cache dump - used to see what caches exist and contents when dumped
    @Scheduled(fixedDelay = 10000)
    public void cacheEvict() {
        cacheManager.getCacheNames().forEach(cacheName -> {
            final Cache         cache       = cacheManager.getCache(cacheName);
            if (log.isTraceEnabled()) {
                Map<String, Object> nativeCache = (Map) cache.getNativeCache();
                nativeCache.forEach((k, v) -> {
                    try {
                        log.trace(String.format("Clearing %s:%s:%s", cacheName, k, objectMapper.writeValueAsString(v)));
                    } catch (JsonProcessingException e) {
                        log.trace("Error", e);
                    }
                });
            }
            Objects.requireNonNull(cache).clear();
        });
    }
}

示例存储库:

@Repository
public interface UserRepository extends BaseRepository<User, Long> {
}
@Repository
public interface TestRepository extends BaseRepository<Test, Long> {
}

来自转储的示例日志语句:

14:06:46.033 [scheduling-1] TRACE CachingConfig - Clearing USER:1:{"id":1,"firstName":"test","lastName":"user"}
14:06:46.033 [scheduling-1] TRACE CachingConfig - Clearing TEST:5:{"id":5,"cool":true}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接