【问题标题】:Spring Data (Hibernate) JPA update of individual fields not visible within transaction事务中不可见的单个字段的 Spring Data (Hibernate) JPA 更新
【发布时间】:2016-08-22 04:02:27
【问题描述】:

我有一个带有布尔标志“已暂停”的实体“工作”:

@Entity
@XmlRootElement(name = "Job")
@Where(clause = "deleted=0")
public class Job {
    ...
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;
    private boolean suspended;
    ...
}

Spring CrudRepository (JPA Hibernate) 用于持久性:

@Repository
public interface JobRepository extends CrudRepository<Job, Integer>, JobStatusSupport {}

我需要单独更新“挂起”标志,而不会覆盖对并发线程中其他字段所做的更新。所以很自然的事情似乎是编写一个只更新“暂停”字段的方法:

public class JobRepositoryImpl implements JobStatusSupport {
    private final static String SET_SUSPENDED = "UPDATE Job SET suspended = :suspended, modificationDate = :modificationDate WHERE id = :id";

    @Override
    public int setSuspended(int id, boolean suspended, Instant modificationDate) {
        int updateCount =  em.createQuery(SET_SUSPENDED)
                .setParameter("suspended", suspended)
                .setParameter("modificationDate", modificationDate)
                .setParameter("id", id)
                .executeUpdate();
        return updateCount;
    }
}

现在我的代码中有以下场景(显然缩短了,实际上这分布在几种方法中,但这个例子确实重现了这个问题):

@Transactional
public void resumeJob(int id) {
    Job jobA = jobRepository.findOne(Integer.valueOf(id));
      // jobA.suspended == true
      // let's set "suspended" to "false"
    int updateCount = jobRepository.setSuspended(id, false, Instant.now());
      // OK: updateCount is 1
    Job jobB = jobRepository.findOne(Integer.valueOf(id));
      // jobB.suspended == true ??? that was just set to "false, wasn't it?
}

可能我缺少一些关于 JPA/Hibernate 的基础知识。但是,这仍然是非常违反直觉的:尽管更新成功并且再次“从数据库”读取数据,为什么 jobB.suspended 仍然是“真”?为什么单个字段的更新在事务中不可见?

(正如人们所期望的那样, 事务完成后,Job.suspended 在数据库中为“假”并用于后续读取。)

如何正确处理这件事?我应该如何编写更新各个字段的代码,以便 JPA 知道所做的事情?像这样简单的事情我是否必须考虑“合并”?

能够编写自己的 SQL 语句对我们的项目至关重要。我正在尝试 Spring Data JPA 主要是为了避免编写大量 CRUD 操作的繁琐工作。但是如果我已经在这个简单的场景中遇到了这样的问题,我想知道使用 JdbcTemplate 是否会更好。

更新:了解 ORM

伙计,我是不是一无所知!我曾在以前使用过 JPA 的项目中工作。但我从来不需要详细处理它(我想知道是否有其他人这样做过)。

编写“更新方法”的全部努力都是徒劳的!我已将其简化为以下内容:

@Transactional
public void resumeJob(int id) {
    Job job = jobRepository.findOne(Integer.valueOf(id));
    job.setSuspended(false);
    job.setName("And Now for Something Completely Different.");
}

就是这样!这会更新数据库和缓存,天知道是什么。单独的 @Transactional 注释足以持久化更改。如果注释被删除,数据库保持不变。所以 ORM 基本上是针对每个人都看到的缓存(通过“附加对象”)工作的。然后希望人们将@Transactional 放在正确的位置(例如,不是放在私有方法上......)并且ORM 机器知道它在做什么(例如,不使缓存更新在打开的事务之外可见)。

老实说,这对我来说似乎有点太神奇了。但现在我知道它的全部内容了,我会试一试。编写大量的 CRUD 方法也不是很吸引人。

如果我弄错了,或者您有最佳实践的链接,请发表评论。 (我开始怀疑是否最好立即分离我从数据库中获得的每个对象,从而破坏 ORM 的全部目的:-)

更新:EntityManager#clear() 足以快速修复

这绝对不是使用 ORM 的聪明方法,但目前我可以在我编写的几个更新方法中简单地调用 clear()。这会使整个缓存无效,并且事务中其他位置的下一次读取会接收更新的数据。当然,正确的做法是简单地修改附加的实体,即“job.setSuspended(false);”。

不需要调用flush(),可能只有当您希望在系统崩溃的情况下将丢失数据的风险降至最低时才会感兴趣。我想 Hibernate 不会立即将完成的事务写入磁盘?

【问题讨论】:

  • JPA 维护托管实例的缓存。读入它们后,您将需要刷新这些实体以反映在 JPA 实体访问之外所做的任何更改。如果您通过批量查询(更新/删除)更改数据库中的数据,那么您应该在将实体读入缓存之前在上下文/事务的开头执行此操作,因为这些语句不会影响托管实体的缓存。或者只是在更新语句之后刷新并清除实体管理器,以便同步任何更改并清除缓存。

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


【解决方案1】:

这是违反直觉的,但如果你仔细想想,这很正常。

  1. 您加载 ID 为 3 的实体。Hibernate 将其存储在其会话缓存中
  2. 您执行更新查询。这个查询对于 Hibernate 来说几乎是一个黑盒子。它无法知道哪些行受到更改的影响,并且您不是通过修改实体来进行这些更改,而是通过直接修改数据库中的行来进行这些更改。因此修改了行,但 ID 为 3 的实体在会话缓存中保持不变
  3. 在同一个会话中再次加载实体。所以 Hibernate 只是返回已经在缓存中的实例,因此不包含更改。

如果你想要一个更新的实体,你有两种解决方案:

  • 通过修改实体来修改数据库,或者
  • clear 更新查询后的缓存。

【讨论】:

  • 是的,有道理。我可以在“resumeJob”方法本身中编写复杂的代码。但是该方法的调用者不必记住更新实体。当我编写数据库访问代码时,我(大部分)知道我在做什么。无需清除()整个数据库缓存。如果我只有它的键(而不是整个实体),有没有办法只清除单个实体的缓存?看来我必须在 cler() 之前执行 flush()?如果您可以扩展您对这些要点的回答,我将很乐意接受。
  • 如果你知道你更新了哪个实体,那么使用 refresh() 来……刷新它。
  • 更正:我的意思是写“我可以在 'setSuspended' 存储库方法本身中编写复杂的代码。”业务方法(resumeJob)不必担心缓存等问题。存储库方法 setSuspended 可以做到这一点。但是,正如更新中提到的,这是完全没有必要的。业务代码可以简单地更新对象而不必担心持久性(只要有人添加了@Transactional)
猜你喜欢
  • 2015-05-25
  • 2013-09-13
  • 2017-10-07
  • 2019-12-20
  • 2018-02-04
  • 2015-04-07
  • 2016-09-12
  • 2021-03-10
  • 2018-08-07
相关资源
最近更新 更多