事务中调用实体save方法(实体合并)后获取实际实体版本
我有一个带有@Version字段的JPA实体:
@Entity public class Cat { @Id private Long id; // 其他字段 @Version private Integer version; }
我需要在事务内多次调用仓库的save方法后,将实体作为快照发送到消息中间件(消息先持久化到数据库,后续再发送)。但遇到的问题是,save后实体的version字段并没有更新为递增后的最新值(比如数据库实际版本已经+1,但内存中的实体还是旧版本),导致发送的快照版本不正确。示例代码如下:
@Transactional public void test(long catId) { Cat cat = catRepo.findById(catId).orElseThrow(); cat.setColor("red"); cat = catRepo.save(cat); // 此处cat的version仍为旧值,比如0 broker.send(cat); // 发送的快照版本不正确 cat.setColor("green"); cat = catRepo.save(cat); broker.send(cat); // 同样拿到旧版本 }
已知version会在flush时更新,但不想通过手动触发flush来解决,有没有其他可行方案?
1. 使用Spring Data JPA的saveAndFlush方法
Spring Data JPA提供了saveAndFlush()方法,它会执行实体保存操作并立即将持久化上下文的变更刷新到数据库,此时返回的实体对象会同步最新的version值。直接替换原有的save方法即可:
@Transactional public void test(long catId) { Cat cat = catRepo.findById(catId).orElseThrow(); cat.setColor("red"); cat = catRepo.saveAndFlush(cat); // 此时cat的version已更新为递增后的值 broker.send(cat); cat.setColor("green"); cat = catRepo.saveAndFlush(cat); broker.send(cat); }
这个方法无需额外操作EntityManager,代码侵入性低,适合大多数场景。
2. 利用EntityManager手动同步状态
如果不想使用saveAndFlush,可以注入EntityManager,在save后先刷新上下文到数据库,再将实体的状态与数据库同步:
@Autowired private EntityManager entityManager; @Transactional public void test(long catId) { Cat cat = catRepo.findById(catId).orElseThrow(); cat.setColor("red"); cat = catRepo.save(cat); entityManager.flush(); // 将变更刷入数据库 entityManager.refresh(cat); // 从数据库同步最新状态,包括version broker.send(cat); // 第二次操作同理 cat.setColor("green"); cat = catRepo.save(cat); entityManager.flush(); entityManager.refresh(cat); broker.send(cat); }
注意:flush会将当前事务内所有未提交的变更刷入数据库,若事务内有其他实体的变更,也会一并提交到数据库,但不会影响事务的原子性。
3. 自定义Repository更新方法并返回版本
可以编写自定义的Repository方法,直接执行更新SQL并返回最新的version,避免依赖实体对象的状态:
public interface CatRepo extends JpaRepository<Cat, Long> { @Modifying @Query("UPDATE Cat c SET c.color = :color WHERE c.id = :id") @QueryHints(@QueryHint(name = org.hibernate.annotations.QueryHints.FLUSH_MODE, value = "ALWAYS")) Integer updateColorAndReturnVersion(@Param("id") Long id, @Param("color") String color); }
调用该方法后,返回值就是更新后的version,之后可以结合findById获取最新实体:
@Transactional public void test(long catId) { Integer newVersion = catRepo.updateColorAndReturnVersion(catId, "red"); Cat cat = catRepo.findById(catId).orElseThrow(); broker.send(cat); // cat的version已更新 newVersion = catRepo.updateColorAndReturnVersion(catId, "green"); cat = catRepo.findById(catId).orElseThrow(); broker.send(cat); }
这个方案适合特定字段的更新场景,避免了对实体对象状态的依赖。
4. 监听JPA的@PostUpdate事件
通过实体监听器监听JPA的PostUpdate事件,该事件会在实体更新并flush到数据库后触发,此时实体的version已经是最新值。可以将发送消息的逻辑放到监听器中:
// 在Cat实体上添加监听器 @EntityListeners(CatUpdateListener.class) @Entity public class Cat { // ... 字段 } // 定义监听器 @Component public class CatUpdateListener { @Autowired private Broker broker; @PostUpdate public void onCatUpdated(Cat cat) { broker.send(cat); // 此时cat的version是最新的 } }
这样业务代码中只需专注于更新实体,发送消息的逻辑由监听器自动处理。需要注意的是,每次flush都会触发该事件,若事务内多次更新,需确保事件触发时机与业务的检查点需求一致。
内容的提问来源于stack exchange,提问作者strelchm

