Spring Boot Data JPA - 修改更新查询 - 刷新持久化上下文

69

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

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

原始JPA Repository:

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();

}

假设我们在数据库中有Email [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();

}

这种方法清除持久性上下文,以避免过时的值,但它仍会放弃实体管理器中所有未刷新的更改。由于我只使用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将所有更改刷新到数据库,然后清除持久性上下文。这样就不会有过时的实体,所有更改都将保存在数据库中。


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


更新2018年

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

@Modifying(flushAutomatically = true, clearAutomatically = true)

3
这里二级缓存不相关,实体被保存在一级缓存中。在执行查询之前清除缓存是适当的解决方案。您可以在Spring-data-JPA bug仓库中提出此主题作为RFE。通过注释,您可以在查询后自动清除,因此我认为自动在查询之前刷新也是很正常的,可以添加一个额外的 flushAutomatically 属性来实现。话虽如此,您也可以保持第一个解决方案,并在执行查询之前显式地进行刷新。 - JB Nizet
1
我在Spring Data JIRA中创建了一个工单DATAJPA-806:为@Modifying注解添加flushAutomatically属性 - ciri-cuervo
1
在Spring Data JPA存储库上创建了拉取请求:https://github.com/spring-projects/spring-data-jpa/pull/172 - ciri-cuervo
4
flushAutomatically已经到了。 - sam
@Modifying(flushAutomatically = true, clearAutomatically = true)挽救了我的一天。我本来要在明年重做我的项目,但这个答案拯救了我。 - Udith Indrakantha
很难相信Sprint Data的开发人员认为在写入数据库时优化性能比实现正确行为更重要。加入这个注释并不能解决我的测试问题。 - Alex Worden
2个回答

12

我知道这不是你问题的直接答案,因为你已经构建了一个修复并在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(), 将会将更改写入数据库。但是如果您有大量的数据,由于需要将所有实体加载到内存中,这种方式可能无法良好运行。但是这是保持Hibernate实体缓存、二级缓存和数据库同步的最佳方法。

这个回答是否说“@Modifying注解是没用的”?不是!如果您确保修改的实体不在本地缓存中,比如只有写操作的应用程序,这种方法就是正确的选择。

仅供参考:您的仓库方法不需要@Transactional

再次提醒:看起来active列直接依赖expire。那为什么不完全删除active,并且在每个查询中只关注expire呢?


7
重要的是要明白,一旦将实体加载到持久化上下文中,它就会被管理,除非您调用refresh(),否则不会从数据库重新加载它(调用find()只会返回已经加载的版本)。更新查询只会更新数据库,而不会更新已经受到管理的任何实体,而delete()查询将更新持久化上下文,因此对使用查询删除的实体进行find()调用将不会返回其实体。 - Klaus Groenbaek

3
正如klaus-groenbaek所说,您可以注入EntityManager并使用其refresh方法:
@Inject
EntityManager entityManager;

...

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

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