这似乎经常出现,但我用谷歌搜索无济于事。
假设您有一个休眠实体User
。数据库中有一个 ID 为 1 的User
。
您有两个线程正在运行,A 和 B。它们执行以下操作:
- A 获取用户 1,
close
其Session
- B 获取用户 1 并
delete
它 - A 更改用户 1 上的字段
- A 获取新
Session
并merge
用户 1
我所有的测试都表明merge
试图在数据库中找到用户 1(显然它不能),因此它插入了一个 id 为 2 的新用户。
另一方面,我的期望是Hibernate会看到被合并的用户不是新的(因为它有一个ID)。它将尝试在数据库中查找用户,这将失败,因此它不会尝试插入或更新。理想情况下,它会引发某种并发异常。
请注意,我使用的是乐观锁定通过@Version
,这无济于事。
所以,问题:
- 我观察到的休眠行为是预期的行为吗?
- 如果是这样,在 JPA
EntityManager
而不是休眠Session
上调用merge
时是否具有相同的行为? - 如果2.的答案是肯定的,为什么没有人抱怨呢?
请参阅下面的休眠文档中的文本。
将给定对象的状态复制到具有相同标识符的持久对象上。如果当前没有与会话关联的持久实例,则将加载该实例。返回持久实例。如果给定实例未保存,请保存 的副本并将其作为新的持久实例返回。
它清楚地指出,复制数据库中对象的状态(数据)。 如果对象不存在,则保存该数据的副本。当我们说保存副本休眠时,请始终使用新标识符创建记录。
休眠合并函数的工作方式如下。
它- 检查实体的状态(附加到或分离到会话)并发现它已分离。
- 然后,它尝试加载带有标识符但在数据库中找不到的实体。
- 由于找不到实体,因此它将该实体视为暂时性实体。
- 暂时性实体始终使用新标识符创建新的数据库记录。
锁定始终应用于附着的实体。如果实体是分离的,则休眠将始终加载它并更新版本值。
锁定用于控制并发问题。这不是并发问题。
我一直在研究JSR-220,Session#merge
声称从中获取其语义。可悲的是,JSR是模棱两可的,我发现。
它确实说:
乐观锁定是一种用于确保更新的技术 对数据库数据进行与实体状态相对应的数据 仅当没有干预事务更新该数据时,自 已读取实体状态。
如果您采用"更新"来包括数据库数据的一般突变,包括删除,而不仅仅是 SQLUPDATE
,我认为您可以提出一个论点,即观察到的行为不符合乐观锁定。
鉴于对我的问题的评论以及随后发现的此错误,许多人都同意。
从纯粹的实际角度来看,无论是否合规,都可能导致相当多的错误,因为它与许多开发人员的期望背道而驰。似乎没有一个简单的解决方法。事实上,Spring Data JPA似乎完全忽略了这个问题,盲目使用EM#merge
。也许其他JPA提供商以不同的方式处理这个问题,但是使用Hibernate可能会导致问题。
我实际上目前正在使用Session#update
来解决这个问题。它真的很丑陋,当你尝试update
一个分离的实体并且已经有一个托管副本时,需要代码来处理这种情况。但是,它也不会导致虚假插入。
我观察到的休眠行为 1.Is 预期行为?
行为是正确的。您只是尝试执行不受并发数据修改保护的操作:)如果必须将操作拆分为两个会话。只需再次找到要更新的对象并检查它是否仍然存在,如果没有,则引发异常。如果有,则使用 em 锁定它。(类、主键、锁定模式类型);或使用@Version或@Entity(optimisticLock=OptimisticLockType.ALL/DIRTY/VERSION)来保护对象,直到事务结束。
2.如果是这样,在 JPA 实体管理器而不是休眠会话上调用合并时的行为是否相同?
可能:是
的3.如果2.的答案是肯定的,为什么没有人抱怨它?
因为如果您使用悲观或乐观锁定来保护您的操作,问题将消失:)
您尝试解决的问题称为:不可重复读取