You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.04 11:27:04