有没有办法完全禁用脏实体的自动更新?
在我们的应用程序中,我们加载一个实体,并且一些“模拟”业务逻辑更改一个属性(对于休眠来说,该实体现在是脏的)。 之后我们要将模拟结果保存到数据库中。我们通过显式调用
resultRepo.save(result)
来做到这一点。在我们的整个项目中,我们使用这种范式,对于我们想要保存的实体,我们明确地称之为 *Repo.save(..)
。
问题是,如果事务结束,Hibernate 现在也会自动保存脏实体。我们怎样才能避免这种隐含的魔力?
可能的解决方案:
Transactional(readOnly=true)
不起作用,因为我们需要存储结果。MANUAL
-> 现在一切都按预期工作,但是
理想情况下会有一个像
org.hibernate.autoSaveDirtyEntites: false
这样的显式切换。
以防万一:我们在 Spring Boot 2.6.4 上使用 Hibernate 5.6.5。
这个常见错误出现在许多代码库中,由于隐藏的脏检查和同步机制,经常让开发人员(包括我自己)感到困惑,像 Spring 的
CrudRepository#save()
这样的名称更增加了混乱。真正的解决方案是从一开始就考虑到这些机制。
有一个解决方法,但我不推荐它,因为它增加了开销并降低了 Hibernate 的优化优势。然而,它可以作为临时修复,直到设计完全考虑 Hibernate 的脏污检测和刷新行为。
在深入研究之前,提及其他本地修复会很有帮助,其中许多您已经注意到:
@Transactional(readOnly=true)
:这会禁用脏检查和刷新,但会阻止保存,因为在此模式下不允许更新。
手动持久化上下文管理:进行修改时分离实体,或仅在需要保存时将其合并回持久化上下文。
使用非实体类:将必要的数据从实体复制到非实体类中,在那里执行操作,并仅在准备保存时将更改应用于实体。
StatelessSession:此选项不涉及持久上下文跟踪或自动刷新,为特定用例提供轻量级替代方案。
注意:将
FlushMode
设置为 MANUAL
并不总能解决问题。虽然它阻止自动刷新(事务提交时除外),但实体仍然可以被检测为脏。在某些情况下,这可能会解决问题,但这取决于具体的代码流程,这里我不会介绍。
在此解决方法中,Hibernate 的脏污检测被调整为将实体视为干净的,直到调用
CrudRepository#save()
为止,此时实体被标记为脏,并立即触发 flush()
。我将概述实现这一目标的一种方法,并简要提及一些替代方法。
为所有要继承的实体定义一个
PersistenceBaseEntity
类,其中 persistable
字段控制何时将实体标记为脏并刷新。
@Getter @Setter
public class PersistenceBaseEntity {
private transient boolean persistable;
}
接下来,我们需要配置Hibernate的脏检测机制。这可以通过实现和注册 Hibernate 的
CustomEntityDirtinessStrategy
来实现。
public class MyCustomEntityDirtinessStrategy extends DefaultCustomEntityDirtinessStrategy {
@Override
public boolean canDirtyCheck(Object entity, EntityPersister persister, Session session) {
return entity instanceof PersistenceBaseEntity;
}
@Override
public boolean isDirty(Object entity, EntityPersister persister, Session session) {
// canDirtyCheck() implementation guarantees that entity is of PersistenceBaseEntity type.
return ((PersistenceBaseEntity) entity).isPersistable();
}
}
要在Spring Boot中注册它,只需添加以下配置:
spring.jpa.properties.hibernate.entity_dirtiness_strategy=<fully-qualified-class-name-of-MyCustomEntityDirtinessStrategy>
最后,自定义
CrudRepository#save()
(和其他相关方法)以通过刷新实体的状态来触发更新。这可以通过提供自定义 JpaRepository
实现来完成,如下所示:
public class MyJpaRepositoryImpl<T, ID> extends SimpleJpaRepository<T, ID> {
public MyJpaRepositoryImpl(JpaEntityInformation<T, ?> entityInformation, EntityManager entityManager) {
super(entityInformation, entityManager);
}
public MyJpaRepositoryImpl(Class<T> domainClass, EntityManager entityManager) {
super(domainClass, entityManager);
}
@Transactional
@Override
public <S extends T> @NonNull S save(@NonNull S entity) {
entity = super.save(entity);
/* Customized behaviour for PersistenceBaseEntities */
if (entity instanceof PersistenceBaseEntity persistenceBaseEntity) {
persistenceBaseEntity.setPersistable(true);
flush();
persistenceBaseEntity.setPersistable(false);
}
return entity;
}
}
考虑重写其他方法,例如
saveAll()
、saveAndFlush()
和 saveAllAndFlush()
,以确保在不同场景下一致地处理实体保存。
在主应用程序类中使用
JpaRepository
注册自定义 @EnableJpaRepositories(repositoryBaseClass=YourCustomRepository.class)
实现。
当然,在最终确定您的方法之前,还有其他值得考虑的替代方法。这里有一些:
FlushEntityEventListener
Interceptor#findDirty()
PreUpdateEventListener
或 PreInsertEventListener