【问题标题】:Spring Boot Data JPA - Modifying update query - Refresh persistence contextSpring Boot Data JPA - 修改更新查询 - 刷新持久性上下文
【发布时间】:2015-11-22 09:29:39
【问题描述】:

我正在使用 Spring Boot 1.3.0.M4 和 MySQL 数据库。

我在使用修改查询时遇到问题,EntityManager 在查询执行后包含过时的实体。

原始 JPA 存储库:

public interface EmailRepository extends JpaRepository<Email, Long> {

    @Transactional
    @Modifying
    @Query("update Email e set e.active = false where e.active = true and e.expire <= NOW()")
    Integer deactivateByExpired();

}

假设我们在数据库中有电子邮件 [id=1, active=true, expire=2015/01/01]

执行后:

emailRepository.save(email);
emailRepository.deactivateByExpired();
System.out.println(emailRepository.findOne(1L).isActive()); // prints true!! it should print false

解决问题的第一种方法:添加clearAutomatically = true

public interface EmailRepository extends JpaRepository<Email, Long> {

    @Transactional
    @Modifying(clearAutomatically = true)
    @Query("update Email e set e.active = false where e.active = true and e.expire <= NOW()")
    Integer deactivateByExpired();

}

此方法清除持久性上下文以不包含过时的值,但它会丢弃所有仍在 EntityManager 中挂起的未刷新更改。因为我只使用 save() 方法而不是 saveAndFlush() 其他实体丢失了一些更改:(


解决问题的第二种方法:存储库的自定义实现

public interface EmailRepository extends JpaRepository<Email, Long>, EmailRepositoryCustom {

}

public interface EmailRepositoryCustom {

    Integer deactivateByExpired();

}

public class EmailRepositoryImpl implements EmailRepositoryCustom {

    @PersistenceContext
    private EntityManager entityManager;

    @Transactional
    @Override
    public Integer deactivateByExpired() {
        String hsql = "update Email e set e.active = false where e.active = true and e.expire <= NOW()";
        Query query = entityManager.createQuery(hsql);
        entityManager.flush();
        Integer result = query.executeUpdate();
        entityManager.clear();
        return result;
    }

}

这种方法的工作方式类似于@Modifying(clearAutomatically = true),但它首先强制 EntityManager 在执行更新之前刷新对 DB 的所有更改,然后清除持久性上下文。这样就不会出现过时的实体,所有更改都将保存在数据库中。


我想知道是否有更好的方法在 JPA 中执行更新语句,而不会出现过时实体的问题,也无需手动刷新到数据库。也许禁用二级缓存?我如何在 Spring Boot 中做到这一点?


2018 年更新

Spring Data JPA 批准了我的 PR,现在 @Modifying() 中有一个 flushAutomatically 选项。

@Modifying(flushAutomatically = true, clearAutomatically = true)

【问题讨论】:

  • 二级缓存在这里无关紧要。实体保存在一级缓存中。之前冲洗是适当的解决方案。您可以在 Spring-data-JPA 错误存储库中将此主题作为 RFE 提出。由于您可以通过注释在查询后自动清除,因此我发现能够通过附加的flushAutomatically 属性在查询之前自动刷新也是正常的。也就是说,您也可以简单地保留您的第一个解决方案,并在执行查询之前显式地刷新。
  • 我在 Spring Data JIRA DATAJPA-806: add flushAutomatically attribute to @Modifying annotation987654321@创建了一张票
  • 在 Spring Data JPA 存储库上创建了拉取请求:github.com/spring-projects/spring-data-jpa/pull/172
  • flushAutomatically 来了
  • @Modifying(flushAutomatically = true, clearAutomatically = true) 拯救了我的一天。我准备明年重复我的项目,这个答案救了我。

标签: spring hibernate spring-boot spring-data spring-data-jpa


【解决方案1】:

我知道这不是您问题的直接答案,因为您已经构建了一个修复程序并在 Github 上发起了拉取请求。谢谢你!

但我想解释一下您可以采用的 JPA 方式。因此,您希望更改与特定条件匹配的所有实体并更新每个实体的值。正常的方法是加载所有需要的实体:

@Query("SELECT * FROM Email e where e.active = true and e.expire <= NOW()")
List<Email> findExpired();

然后遍历它们并更新值:

for (Email email : findExpired()) {
  email.setActive(false);
}

现在,hibernate 知道所有更改,如果事务完成或您手动调用EntityManager.flush(),会将它们写入数据库。我知道如果您有大量数据条目,这将无法正常工作,因为您将所有实体都加载到内存中。但这是保持休眠实体缓存、二级缓存和数据库同步的最佳方式。

这个答案是不是说“`@Modifying´ 注释没用”?不!如果您确保修改后的实体不在本地缓存中,例如只写应用程序,这种方法就是要走的路。

仅作记录:您的存储库方法不需要@Transactional

仅用于记录 v2:active 列看起来直接依赖于 expire。那么为什么不完全删除 active 并在每个查询中只查看 expire 呢?

【讨论】:

  • 重要的是要理解,一旦实体被加载到持久性上下文中,它就会被管理,并且不会从数据库中重新加载,除非您调用 refresh()(调用 find() 只会返回已经加载版本)。更新查询只会更新数据库,而不是任何已管理的实体,而 delete() 查询将更新持久性上下文,因此对 find() 的调用将不会返回被查询删除的实体。..
【解决方案2】:

正如 klaus-groenbaek 所说,您可以注入 EntityManager 并使用它的刷新方法:

@Inject
EntityManager entityManager;

...

emailRepository.save(email);
emailRepository.deactivateByExpired();
Email email2 = emailRepository.findOne(1L);
entityManager.refresh(email2);
System.out.println(email2.isActive()); // prints false

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-10-31
    • 2021-09-28
    • 2013-04-01
    • 1970-01-01
    • 2015-03-03
    • 2020-03-29
    • 1970-01-01
    相关资源
    最近更新 更多