如何告诉Hibernate的二级缓存使用正确的ID?

vi4fp9gy  于 2023-11-21  发布在  其他
关注(0)|答案(2)|浏览(131)

在使用Hibernate 6和Ehcache 3的Sping Boot 3应用程序中,我遇到了一个奇怪的问题。我的实体有一个id属性,该属性的属性名以实体名称为前缀,所以例如Display实体将有一个名为displayIdid
带有缓存注解的实体如下所示:

@Entity
@Access(AccessType.FIELD)
@org.hibernate.annotations.Cache(region = "display-cache",
                                 usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
@Table(name = "display")
public class Display {

    @Id
    @Column(name = "display_id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long displayId;

    @Column(name = "description")
    private String description;

    //...
}

字符串
现在,只要我使用JpaRepository的内置findById()方法查询显示,一切都很好,显示会按预期进行缓存。但是当我尝试查询id属性字段displayId本身时,没有缓存,尽管参数是实体的id
JpaRepository看起来像这样:

public interface DisplayRepository extends JpaRepository<Display, Long> {

    Optional<Display> findByDisplayId(Long displayId);

}


查询如下所示:

displayRepository.findByDisplayId(id);  // cache is not working

// just for comparison:
displayRepository.findById(id);         // cache is working


所以我的问题是:
我如何告诉Hibernate使用的显示ID是实体的ID,以便Hibernate按预期进行缓存?

8gsdolmq

8gsdolmq1#

在Hibernate中实现二级缓存并不像你在某人的博客文章中读到的那样简单,经验法则如下:如果你能够在服务/业务级别实现缓存-不要回头,不要使用二级缓存。
以下是我根据以前的研究得出的一些想法:

.实现是错误的,没有人会去修复:2nd-level cache does not work as expected,值得注意的是,我只使用集成测试发现了这些问题,所以,我不知道为什么这个功能没有被Hibernate项目中的测试所覆盖。
II.您需要考虑如何处理缓存中的陈旧数据:您可能会用旧数据覆盖实际数据,或者基于旧数据做出错误的决策,这两种情况看起来都不太好。最直接的方法是通过@Version字段启用版本戳检查,但是,“版本检查”完全是另一个世界,你可能会面临以前从未遇到过的挑战(另一方面,我无法理解有人如何使用JPA而不进行版本检查)
III.不要将spring的缓存功能与JPA仓库一起使用:spring不是为缓存可变数据而设计的,JPA实体在设计上是可变的,而不是性能提升,你会在DB中得到错误的数据。
IV.如果应用程序通过update(JPA存储库中的@Modifying@Query(update/delete))修改实体,这些操作会使缓存失效-避免使用此类模式
V.查询缓存完全不工作,原因如下:

1.如果您正在检索实体,HBN会缓存相应的标识符,连续的缓存命中将使用这些缓存的ID逐个检索实体,慢慢地
1.修改来自同一查询空间的实体(即,由与要缓存的查询中涉及的表相同的表支持的实体)使查询缓存无效- HBN无法弄清楚要更新/删除的实体是否影响查询结果,所以它使所有内容无效。

VI.全局/分布式缓存似乎没有用,本地缓存接受远程无效消息似乎没问题:问题是如果实体没有“很多”关联,通过单个查询从DB检索它不应该比从远程缓存检索它慢,所以,从用户体验的Angular 来看,全局/分布式缓存没有改进。
VII.我相信通过实体类的注解来控制缓存行为的想法是完全错误的,要点如下:实体只是定义数据,然而关于可能的优化和数据一致性的假设是特定应用程序的责任,所以,在我看来,设置缓存的最佳选择是利用org.hibernate.integrator.spi.Integrator,例如:

@Override
public void integrate(Metadata metadata, SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry) {
    for (PersistentClass persistentClass : metadata.getEntityBindings()) {
        if (persistentClass instanceof RootClass) {
            RootClass rootClass = (RootClass) persistentClass;
            if ("myentity".equals(rootClass.getEntityName())) {
                rootClass.setCached(true);
                rootClass.setCacheRegionName("myregion");
                rootClass.setCacheConcurrencyStrategy(AccessType.NONSTRICT_READ_WRITE.getExternalName());
            }
        }
    }
}

字符串

VIII在Hibernate中实现二级缓存最安全的方法如下:

1.首先,让HBN来填充二级缓存:

@Bean
public HibernatePropertiesCustomizer hibernateSecondLevelCacheCustomizer() {
    return map -> {
        map.put(AvailableSettings.JPA_SHARED_CACHE_RETRIEVE_MODE, CacheRetrieveMode.BYPASS);
        map.put(AvailableSettings.JPA_SHARED_CACHE_STORE_MODE, CacheStoreMode.USE);
    };
}


1.之后,如果您认为合适,可以调用EntityManager#find(Class<T>, Object, Map<?,?>)方法,并将AvailableSettings#JAKARTA_JPA_SHARED_CACHE_RETRIEVE_MODE属性设置为CacheRetrieveMode#USE
关于你的问题...
在Hibernate中有几个选项可以通过id检索实体:

  1. EntityManager#find-最常见的一个,同时支持二级和一级缓存
  2. EntityManager#getReference-不是检索实体,而是创建代理对象,在某些情况下它可能有用,但是HBN实现似乎被破坏了:连续调用EntityManager#find返回代理对象而不是全功能实体
  3. Session#byMultipleIds-允许批量检索相同类型的实体,同时支持二级和一级缓存,不幸的是,JPA存储库不支持
  • 通过JPQL查询,如select e from entity e where e.id=:id-最奇怪的选项来做简单的事情:
  • 当启用自动刷新时(这实际上是大多数Hibernate应用程序的合理默认值),Hibernate倾向于保持DB状态与持久化上下文同步,这反过来意味着在执行任何JPQL查询之前,Hibernate将检查持久化上下文是否包含脏实体(这需要一些时间)并将这些脏实体刷新到DB中。
  • 如果要检索实体已经存在于持久化上下文中,Hibernate不会使用DB数据刷新其状态,这种行为似乎很奇怪

从JPA存储库的Angular 来看,每个声明的方法,不是default,也不是由基本存储库(大多数情况下是SimpleJpaRepository)或fragment实现的,都是由JPQL查询备份的,因此在某些情况下可能无法按预期/期望工作。
因此,对于特定情况,最好的选择是给予使用命名约定,这会导致性能问题,如果这是不可能的,您可以利用使用default方法:

default Optional<Display> findByDisplayId(Long displayId) {
   return findById(displayId);
}

lkaoscv7

lkaoscv72#

当使用findByDisplayId(id)查询displayId时该高速缓存似乎无法将displayId识别为实体的标识符,并且无法按预期运行,这与使用findById(id)时的情况不同,findById(id)运行良好。
一种可能的解决方案是使用Hibernate's @NaturalId annotation,也可以使用mentioned here
自然ID是一个不可变的业务关键字,它在特定数据库表的范围内是唯一的。在您的示例中,displayId充当此业务关键字。
但是,由于displayId * 已经 * 用@Id进行了注解,表明它是实体的主键:在同一字段上用自然ID重载标识概念可能会导致混淆和意外行为。
另一种可能:创建一个显式利用Hibernate's 2nd Level Cache的自定义查询方法,将@Query annotation@Cacheable注解一起使用,以确保利用了缓存(如“Caching in Hibernate“中的Himani Prasad所示)。
然而:
第二种方法是我在一些特殊情况下使用的经典的查询缓存方法。我可以用这种方法来解决问题,但这意味着需要两个缓存,而查询缓存的速度比单独使用专用实体缓存要慢一些。
使用@EntityGraph注解来提示Hibernate以快取友好的方式撷取实体,即使与QueryHints结合使用也是如此。@QueryHintsThorben Janssen的“11 JPA and Hibernate query hints every developer should know”中提到。
但在这里不起作用。目标是确保在通过displayId查询时使用Hibernate的二级缓存,而不求助于查询缓存。
Andrey B. Panfilov在评论中建议
default Optional<Display> findByDisplayId(Long displayId) {return findById(displayId);}的一个。
当HBN缓存查询时,它只缓存id,所以,不管你提供了什么提示,它总是调用em.find(),而且,这样的“缓存”在任何相应类型的实体被保存时都是无效的,这样的缓存完全没用。
通过将findByDisplayId方法委托给JpaRepository的内置findById方法,这确实是一种利用Hibernate二级缓存的简单方法。这样可以确保在displayId查询时使用em.find()方法(缓存感知)。

public interface DisplayRepository extends JpaRepository<Display, Long> {

    default Optional<Display> findByDisplayId(Long displayId) {
        return findById(displayId);
    }

}

字符串
当缓存查询时,Hibernate只缓存实体标识符,而不缓存实体本身,并且只要保存了相应类型的任何实体该高速缓存就会失效。
Andrey在“JPA Query with several different @Id columns“中详细介绍了该方法,其中包括:

default Optional<T> findByIdAndType(K id, String type) {
    return findById(id)
            .filter(e -> Objects.equals(e.getType(), type));
}


EntityManager#find方法将是通过ID检索实体的最有效的方法:它支持Spring Data JPA中的CrudRepository#findById方法,后者旨在有效地利用Hibernate的第二级缓存。
从评论中:不是通过entityManager.find()进行的任何数据库调用都不会被Hibernate的一级或二级缓存自动缓存,而是被视为自定义查询调用。
这些方法包括派生的JPA查询方法和自定义JPA方法,即使它们使用@Id带注解的主键。
Hibernate的缓存机制直接绑定到JPA存储库中的findById()方法,这不是Hibernate的“问题”,而是spring-data-jpa的设计方式。
虽然注解不能直接影响Hibernate将自定义查询方法视为可缓存的find操作,但在存储库接口中使用默认方法(委托给findById())是一种通过Hibernate和Spring Data JPA实现所需缓存行为的惯用方法。

相关问题