在使用Hibernate 6和Ehcache 3的Sping Boot 3应用程序中,我遇到了一个奇怪的问题。我的实体有一个id
属性,该属性的属性名以实体名称为前缀,所以例如Display实体将有一个名为displayId
的id
。
带有缓存注解的实体如下所示:
@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按预期进行缓存?
2条答案
按热度按时间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
,例如:字符串
VIII在Hibernate中实现二级缓存最安全的方法如下:
1.首先,让HBN来填充二级缓存:
型
1.之后,如果您认为合适,可以调用
EntityManager#find(Class<T>, Object, Map<?,?>)
方法,并将AvailableSettings#JAKARTA_JPA_SHARED_CACHE_RETRIEVE_MODE
属性设置为CacheRetrieveMode#USE
关于你的问题...
在Hibernate中有几个选项可以通过id检索实体:
EntityManager#find
-最常见的一个,同时支持二级和一级缓存EntityManager#getReference
-不是检索实体,而是创建代理对象,在某些情况下它可能有用,但是HBN实现似乎被破坏了:连续调用EntityManager#find
返回代理对象而不是全功能实体Session#byMultipleIds
-允许批量检索相同类型的实体,同时支持二级和一级缓存,不幸的是,JPA存储库不支持select e from entity e where e.id=:id
-最奇怪的选项来做简单的事情:从JPA存储库的Angular 来看,每个声明的方法,不是
default
,也不是由基本存储库(大多数情况下是SimpleJpaRepository
)或fragment
实现的,都是由JPQL查询备份的,因此在某些情况下可能无法按预期/期望工作。因此,对于特定情况,最好的选择是给予使用命名约定,这会导致性能问题,如果这是不可能的,您可以利用使用
default
方法:型
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
结合使用也是如此。@QueryHints
在Thorben 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()
方法(缓存感知)。字符串
当缓存查询时,Hibernate只缓存实体标识符,而不缓存实体本身,并且只要保存了相应类型的任何实体该高速缓存就会失效。
Andrey在“JPA Query with several different @Id columns“中详细介绍了该方法,其中包括:
型
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实现所需缓存行为的惯用方法。