Spring Boot JPA更新OneToOne关联后无法刷新关联实体求助
JPA关联实体刷新问题排查与解决
配置与实体定义
application.properties配置
spring.datasource.url=jdbc:postgresql://127.0.0.1:5432/mydatabase spring.datasource.username=**** spring.datasource.password=**** spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.show-sql=false spring.jpa.open-in-view=false spring.jpa.hibernate.ddl-auto=create spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
Rcon实体定义
@Entity @Table(name = "rcon", uniqueConstraints = @UniqueConstraint(columnNames = { "rcon_host", "rcon_port" })) public class Rcon{ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id", columnDefinition = PostgreSqlColumn.POSTGRESQL_BIGINT, nullable = false) private Long id; @Column(name = "name", columnDefinition = PostgreSqlColumn.POSTGRESQL_VARCHAR_255, nullable = false, unique = true) private String name; // 其他常规字段 }
Console实体定义(与Rcon为OneToOne关联)
@Entity @Table(name = "console") public class Console { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id", columnDefinition = PostgreSqlColumn.POSTGRESQL_BIGINT, nullable = false) private Long id; @OneToOne(optional = false, fetch = FetchType.EAGER, cascade = {CascadeType.MERGE, CascadeType.REFRESH}, orphanRemoval = false) @JoinColumn(name = "rcon_id", nullable = false) @OnDelete(action = OnDeleteAction.CASCADE) private Rcon rcon; // 其他常规字段 }
问题场景
- 保存新Rcon实体(名称为foo):正常执行
// RconServiceImpl @Override @Transactional public Rcon save(Rcon rcon) { var isNew = rcon.getId() == null; var entity = repository.saveAndFlush(rcon); if(isNew) { this.eventProducer.produce(RconEvent.notifyCreated(this, entity)); } else { this.eventProducer.produce(RconEvent.notifyUpdated(this, entity)); } return entity; } // 调用代码 var newRcon= new Rcon.Builder().withName("foo").build(); rconService.save(newRcon);
- 保存关联该Rcon的Console实体:正常执行
// ConsoleServiceImpl @Override @Transactional public Console save(Console console) { var isNew = console.getId() == null; var entity = repository.save(console); if(isNew) { this.eventProducer.produce(ConsoleEvent.notifyCreated(this, entity)); } else { this.eventProducer.produce(ConsoleEvent.notifyUpdated(this, entity)); } return entity; } // 调用代码 var rcon= rconService.findByName("foo").orElseThrow(...); var newConsole = new Console.Builder().withRcon(rcon).build(); consoleService.save(newConsole);
- 更新Rcon实体名称为bar,触发RconEvent更新事件:正常执行
var rcon= rconService.findByName("foo").orElseThrow(...); rcon.setName("bar"); rconService.save(rcon);
- 监听RconEvent时,Console关联的Rcon名称未更新:
@EventListener protected void onRconEvent(RconEvent event) { var rcon = event.getRcon(); if(event.isUpdated()) { rconService.findByRconId(rcon.getId()).ifPresent(console -> { logger.error("console = {}", console); // 此处console.getRcon().getName()为bar,符合预期 }); this.doSomething(rcon).thenRun(() -> rconService.findByRconId(rcon.getId()).ifPresent(console -> { logger.error("console = {}", console); // 此处console.getRcon().getName()仍为foo,不符合预期 })); } }
尝试的解决方案
- 尝试在
findByRconId方法中刷新EntityManager、为@OneToOne添加CascadeType.REFRESH、将Rcon的save改为saveAndFlush,均无效:
@Override @Transactional(readOnly = true) public Optional<Console> findByRconId(Long rconId) { return repository.findByRconId(rconId).map(console -> { if (entityManager.contains(console)) { entityManager.refresh(console); } return console; }); }
- 修改为detach后重新查询的方法虽有改善,但仍存在偶尔不更新的情况:
@Override @Transactional(readOnly = true) public Optional<Console> findByRconId(Long rconId) { return repository.findByRconId(rconId).map(console -> { entityManager.detach(console); return entityManager.find(Console.class, console.getId()); }); }
问题分析与解决
核心原因
问题出在Hibernate的一级缓存(Session缓存) 和异步操作的事务上下文隔离:
- 异步方法
this.doSomething(rcon).thenRun(...)在独立线程执行,该线程的EntityManager/Session是全新的,但如果之前线程已将Console或Rcon实例加载到二级缓存,新线程可能读取到未刷新的缓存数据。 entityManager.refresh(console)默认只刷新Console本身,不会递归刷新关联的Rcon实体。
彻底解决方案
方案1:刷新Console时递归刷新关联的Rcon
修改findByRconId方法,强制刷新关联的Rcon实体:
@Override @Transactional(readOnly = true) public Optional<Console> findByRconId(Long rconId) { return repository.findByRconId(rconId).map(console -> { entityManager.refresh(console, LockModeType.READ); // 加读锁确保从数据库读取最新数据 entityManager.refresh(console.getRcon()); // 显式刷新关联的Rcon return console; }); }
方案2:禁用二级缓存(若无需使用)
在实体上显式禁用缓存:
@Entity @Cacheable(false) @Table(name = "rcon", ...) public class Rcon { ... } @Entity @Cacheable(false) @Table(name = "console", ...) public class Console { ... }
或全局禁用二级缓存:
spring.jpa.properties.hibernate.cache.use_second_level_cache=false spring.jpa.properties.hibernate.cache.use_query_cache=false
方案3:异步操作中使用原生SQL绕过缓存
在异步回调中直接用原生SQL查询,避免缓存干扰:
this.doSomething(rcon).thenRun(() -> { String sql = "SELECT c.* FROM console c JOIN rcon r ON c.rcon_id = r.id WHERE r.id = ?"; Console console = entityManager.createNativeQuery(sql, Console.class) .setParameter(1, rcon.getId()) .getSingleResult(); logger.error("console = {}", console); // 读取到最新Rcon名称 });
方案4:更新Rcon后清除二级缓存
若必须保留二级缓存,在Rcon保存后手动清除对应缓存条目:
// RconServiceImpl的save方法中添加缓存清除逻辑 @Override @Transactional public Rcon save(Rcon rcon) { var isNew = rcon.getId() == null; var entity = repository.saveAndFlush(rcon); if(isNew) { this.eventProducer.produce(RconEvent.notifyCreated(this, entity)); } else { entityManager.getEntityManagerFactory().getCache().evict(Rcon.class, entity.getId()); this.eventProducer.produce(RconEvent.notifyUpdated(this, entity)); } return entity; }
关键注意事项
- 异步操作的事务上下文与原操作完全隔离,不能依赖原线程的缓存状态。
@OneToOne的CascadeType.REFRESH仅在主动刷新Console时触发Rcon刷新,不会自动监听Rcon的更新。- 二级缓存是全局共享的,更新实体后必须同步清除对应缓存,否则其他线程会读取旧数据。
内容的提问来源于stack exchange,提问作者Florian S.
相关产品推荐
相关产品推荐

