使用Hibernate Envers时两次更新中间查询导致审计表版本缺失问题
问题描述
我基于JPA使用Hibernate Envers的@Audited注解实现审计功能时出现异常:当两次实体更新操作中间执行实体查询时,审计表会跳过一个版本记录。当前使用的组件版本为Hibernate 5.5.7、MSSQL Server 15、Java 1.8。
复现步骤
- 创建实体,执行flush并提交事务(实体带有标注
@Version的版本字段) - 开启新事务
- 查询获取实体,修改某一属性(例如修改name字段),不执行flush
- 再次查询获取同一实体,修改另一属性(例如修改id字段)
- 提交事务
预期结果
审计表中的记录数应当与实体version字段的数值匹配,例如实体最终version为2时,审计表应当存在version为0、1、2的3条对应记录。
实际结果
审计表仅存在version=0和version=2的两条记录,version=1的记录缺失。
问题分析
初步判断该问题是因为步骤3中执行查询时Hibernate触发了自动flush,实体变更导致version字段更新,但该版本对应的审计记录未生成。
请问有什么解决方案可以让审计表保存这个“缺失”的版本?还是我在配置@Audit审计表时遗漏了什么配置项?
相关代码
Main.java
package se.navod.platform.test.hibernate; import java.util.List; import java.util.concurrent.ThreadLocalRandom; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.EntityTransaction; import javax.persistence.Persistence; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; public class Main { private static EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("PlatformTest"); public static void main(String[] args) { EntityTransaction transaction = null; try { int id = ThreadLocalRandom.current().nextInt(0, Integer.MAX_VALUE - 1); String name = "Name_" + id; create(id, name, 10);// 创建学生实体 EntityManager manager = entityManagerFactory.createEntityManager(); transaction = manager.getTransaction(); transaction.begin();// 开启事务 Student student1 = findStudentByPropertyValue(manager, name, "name");// 通过name查询学生 student1.setName("NewName");// 修改name属性 // 再次通过id查询同一个学生 Student student2 = findStudentByPropertyValue(manager, "" + id, "studentId"); student2.setAge(50);// 修改age属性 manager.persist(student2);// 持久化并提交 transaction.commit(); } catch (Exception e) { e.printStackTrace(); if (transaction != null) { transaction.rollback(); } } finally { if (entityManagerFactory != null) { entityManagerFactory.close(); } } } public static Student create(int id, String name, int age) { EntityManager manager = entityManagerFactory.createEntityManager(); EntityTransaction transaction = null; try { transaction = manager.getTransaction(); transaction.begin(); Student stu = new Student(id, name, age); manager.persist(stu); transaction.commit(); return stu; } catch (Exception ex) { if (transaction != null) { transaction.rollback(); } ex.printStackTrace(); throw ex; } finally { manager.close(); } } private static Student findStudentByPropertyValue(EntityManager manager, String value, String property) { CriteriaBuilder criteriaBuilder = manager.getCriteriaBuilder(); CriteriaQuery<Student> criteriaQuery = criteriaBuilder.createQuery(Student.class); Root<Student> itemRoot = criteriaQuery.from(Student.class); Predicate predicate = criteriaBuilder.equal(itemRoot.get(property), value); criteriaQuery.where(predicate); criteriaQuery.orderBy(criteriaBuilder.asc(itemRoot.get("id"))); List<Student> items = manager.createQuery(criteriaQuery).getResultList(); return items.size() == 0 ? null : items.get(0); } }
实体类 Student.java
package se.navod.platform.test.hibernate; import java.io.Serializable; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; import javax.persistence.Version; import org.hibernate.envers.Audited; @Entity @Table(name = "student") @Audited public class Student implements Serializable { private static final long serialVersionUID = -834531085311709309L; @Column(name = "student_id", unique = true) private int studentId; @Id @Column(name = "primaryKey") @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; @Column(name = "student_name", nullable = false) private String name; @Column(name = "student_age", nullable = false) private int age; @Version private long version; public Student() { super(); } public Student(int studentId, String name, int age) { super(); this.studentId = studentId; this.name = name; this.age = age; } public void setId(int id) { this.id = id; } public long getVersion() { return version; } public void setVersion(long version) { this.version = version; } public int getStudentId() { return studentId; } public void setStudentId(int studentId) { this.studentId = studentId; } public int getId() { return id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return id + "\t" + name + "\t" + age; } }
persistence.xml配置文件
<persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd" version="2.0"> <persistence-unit name="PlatformTest" transaction-type="RESOURCE_LOCAL"> <!-- Persistence provider --> <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider> <!-- Entity classes --> <class>se.navod.platform.test.hibernate.Student</class> <properties> <property name="javax.persistence.jdbc.url" value="jdbc:jtds:sqlserver://localhost:1433/Test;prepareSQL=0;" /> <!-- 数据库用户名 --> <property name="javax.persistence.jdbc.user" value="spider3" /> <!-- 数据库密码 --> <property name="javax.persistence.jdbc.password" value="spider3" /> <property name="hibernate.dialect" value="org.hibernate.dialect.SQLServer2012Dialect" /> <property name="org.hibernate.envers.audit_table_suffix" value="_History" /> <property name="org.hibernate.envers.do_not_audit_optimistic_locking_field" value="false" /> <property name="org.hibernate.envers.revision_field_name" value="revision" /> <property name="org.hibernate.envers.revision_type_field_name" value="revisionType" /> </properties> </persistence-unit> </persistence>
数据库建表脚本
USE [Test] GO /****** Object: Table [dbo].[student] Script Date: 9/13/2021 12:07:13 PM ******/ SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO CREATE TABLE [dbo].[student]( [student_id] [int] NULL, [student_name] [nchar](1000) NULL, [student_age] [int] NULL, [primaryKey] [bigint] IDENTITY(1,1) NOT NULL, [version] [bigint] NOT NULL, CONSTRAINT [PK_persionId] PRIMARY KEY NONCLUSTERED ( [primaryKey] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY] ) ON [PRIMARY] GO CREATE TABLE [dbo].[student_History]( [student_id] [int] NULL, [student_name] [nchar](1000) NULL, [student_age] [int] NULL, [primaryKey] [bigint] NOT NULL, [version] [bigint] NOT NULL, [revision] [int] NOT NULL, [revisionType] [tinyint] NULL ) ON [PRIMARY] GO CREATE TABLE [dbo].[REVINFO]( [id] [int] NULL, [REVTSTMP] [bigint] NOT NULL ) ON [PRIMARY] GO
内容的提问来源于stack exchange,提问作者Eranda Ediriweera
相关产品推荐
相关产品推荐

