Hibernate Envers单向OneToMany级联后审计查询抛HibernateException如何解决?
这个问题我之前也碰到过,核心原因是单向OneToMany关联配合@JoinColumn使用时,级联保存会触发两次Car实体的状态变更,导致Envers生成了两条同一版本、同一Car ID的审计记录,查询时Hibernate无法映射出唯一的实体版本,从而抛出异常。
下面给你两种靠谱的修复方案:
方案1:改用双向关联(推荐,符合JPA规范)
单向OneToMany用@JoinColumn的方式在Envers中很容易出现这类问题,改成双向关联不仅更符合JPA的设计规范,还能让Envers正确处理关联的审计逻辑:
首先修改Car实体,添加反向的@ManyToOne关联:
@Entity @Audited public class Car { @Id @GeneratedValue private Long id; private String brand; @ManyToOne @JoinColumn(name = "DRIVER_ID") private Driver driver; // 构造方法、getter、setter }
然后调整Driver实体的关联配置,去掉@AuditJoinTable,改用mappedBy指定反向关联属性:
@Entity @Audited public class Driver { @Id @GeneratedValue private Long id; @OneToMany(mappedBy = "driver", cascade = CascadeType.ALL, orphanRemoval = true) private Set<Car> cars; // 构造方法、getter、setter }
这样当你级联保存Driver和Car时,Hibernate会先插入Driver,再插入带有DRIVER_ID的Car,只会触发一次Car的插入操作,Envers也只会生成一条Car的审计记录,查询Driver的审计版本时就不会再抛出异常了。
方案2:保留单向关联,调整保存逻辑(不推荐,破坏封装)
如果你因为某些原因必须保留单向关联,可以通过手动控制保存顺序来避免重复的审计记录,但这种方法会破坏JPA的关联封装,仅作为临时方案:
- 给
Car实体添加DRIVER_ID字段的setter(或者直接添加字段):
@Entity @Audited public class Car { @Id @GeneratedValue private Long id; private String brand; // 新增DRIVER_ID字段 private Long driverId; // 构造方法、getter、setter }
- 手动分步骤保存Driver和Car,避免级联带来的两次状态变更:
// 先保存Driver,获取其ID entityManager.persist(driver); entityManager.flush(); // 确保Driver的ID已生成 // 给每个Car设置Driver的ID,再保存 for (Car car : driver.getCars()) { car.setDriverId(driver.getId()); entityManager.persist(car); }
不过这种方法需要修改实体结构,违背了JPA关联的设计初衷,除非万不得已不建议使用。
问题根源拆解
你当前的代码执行entityManager.persist(driver);时,Hibernate的执行流程是:
- 插入Driver记录,生成ID=1
- 插入Car记录,此时DRIVER_ID为null(因为Driver还未关联)
- 更新Car的DRIVER_ID为1,完成关联
这两次操作(插入+更新)都会被Envers捕获,所以CAR_AUD表生成了两条记录:一条是插入时的状态(DRIVER_ID=null),一条是更新后的状态(DRIVER_ID=1)。当你查询Driver的审计版本时,Envers需要关联查询Car的审计记录,但同一版本(REV=8)下同一Car ID(ID=3)存在两条记录,Hibernate无法确定哪一条是该版本下的正确状态,因此抛出了More than one row with the given identifier was found异常。
内容的提问来源于stack exchange,提问作者Uladzislau Stryzhonak

