使用 Hibernate Envers 进行实体审计

业务应用程序中的常见要求是在特定数据更改时存储版本控制信息;当某事发生变化时,谁改变了它,改变了什么。在这篇博文中,我们将介绍Hibernate Envers,它是Hibernate JPA库的一个组件,它为实体类提供了一个简单的审计/版本控制解决方案。Envers 可与 Hibernate 和 JPA 配合使用,您可以在 Hibernate 工作的任何地方使用 Envers。

Envers 将更改存储在特殊的审计表中,并提供多种查询方法来访问历史快照。

设置

若要开始使用 Envers,首先需要将库添加到项目的类路径中。在 Maven 托管文件中,添加此依赖项。

    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-envers</artifactId>
      <version>5.4.6.Final</version>
    </dependency>

绒球.xml

接下来,使用 @Audited注释批注 Envers 应跟踪的实体类或实体属性。对于以下示例,我使用两个实体类:员工和公司。在 Employee 类中,我添加了类,Envers 跟踪此类中的所有属性。@Audited

@Entity
@Audited(withModifiedFlag = true)
public class Employee {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private int id;

  private String lastName;

  private String firstName;

  private String street;

  private String city;

  @ManyToOne
  private Company company;

员工.java

在公司类中,我只添加到属性。Envers 会跟踪此属性并忽略所有其他属性。Envers 还为添加到类但想要忽略特定属性的情况提供了@NotAudited注释。@Auditedname@Audited

@Entity
public class Company {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private int id;

  @Audited
  private String name;

  private String street;

  private String city;

  @OneToMany(mappedBy = "company", cascade = CascadeType.ALL, orphanRemoval = true)
  private Set<Employee> employees;

公司.java

确保实体使用不可变的唯一标识符(主键)。

在下面的示例项目中,我将休眠配置设置为。hibernate.hbm2ddl.autoupdate

      <property name="hibernate.hbm2ddl.auto" value="update" />

坚持不懈.xml

完成此配置后,Hibernate会自动创建“员工”和“公司”两个表。对于每个实体,它创建一个审核表,Envers 在其中跟踪更改。它还创建 REVINFO 表,该表跟踪修订号和发生更改时的时间戳。@Audited

每次要插入、更新或删除实体时,Envers 都会介入并通过在 REVINFO 表和相应的 AUD 表中插入新行来创建新的修订。@Audited

请注意,EMPLOYEE_AUD表包含 EMPLOYEE 表中每个字段的字段,而COMPANY_AUD表仅包含字段,这是因为我们只在 Company 类中批注了属性。AUD 表的 ID 字段是相应实体表的主键;这就是主键必须是不可变的原因。namename

在 Employee 类中,我们启用了默认情况下禁用的withModifiedFlag选项 ()。您可以在EMPLOYEE_AUD表中看到此选项的效果。Envers 为每个属性添加了额外的布尔属性_MOD。此修改标志存储属性在给定修订时已更改的信息。@Audited(withModifiedFlag = true)

在公司类中,我们未启用此选项。因此,COMPANY_AUD表不包含NAME_MOD字段。

为了进行比较,如果不启用该选项,则EMPLOYEE_AUD表定义:

仅当您需要此信息时,才应启用,因为代价是附加字段会增加审核表的大小。有一个 Envers 查询 () 依赖于此附加信息,因此,如果计划使用此查询方法,则必须启用该选项。withModifiedFlagforRevisionsOfEntityWithChange

自定义修订实体

请注意,默认情况下,Envers仅跟踪发生更改的日期和时间。但是,在多用户应用程序中,您通常还想知道谁进行了更改。

为此,我们需要创建自定义修订实体类。此类必须扩展DefaultRevisionEntity类,并且必须使用 and@RevisionEntity 进行批注。您可以添加任何您喜欢的属性,它们将与修订号和时间戳一起存储在 REVINFO 表中。@Entity

import javax.persistence.Entity;
import javax.persistence.Table;

import org.hibernate.envers.DefaultRevisionEntity;
import org.hibernate.envers.RevisionEntity;

@Entity
@Table(name = "REVINFO")
@RevisionEntity(CustomRevisionEntityListener.class)
public class CustomRevisionEntity extends DefaultRevisionEntity {

  private static final long serialVersionUID = 1L;

  private String username;

  public String getUsername() {
    return this.username;
  }

  public void setUsername(String username) {
    this.username = username;
  }
}

自定义修订实体.java

我们指定的侦听器必须实现RevisionListener接口。我们只需要实现一种方法:在此方法中,我们需要填写其他属性,在本例中为用户名。我们不必触摸修订号和时间戳,Envers会自动设置它们。@RevisionEntitynewRevision

import org.hibernate.envers.RevisionListener;

public class CustomRevisionEntityListener implements RevisionListener {

  @Override
  public void newRevision(Object revisionEntity) {
    CustomRevisionEntity customRevisionEntity = (CustomRevisionEntity) revisionEntity;
    customRevisionEntity.setUsername(CurrentUser.INSTANCE.get());
  }
}

CustomRevisionEntityListener.java

对于此演示应用程序,我们将登录用户存储到 ThreadLocal 变量中。上面的侦听器提取它,我们将用户名设置为CurrentUser.INSTANCE.get()CurrentUser.INSTANCE.set(....)

public class CurrentUser {

  public static final CurrentUser INSTANCE = new CurrentUser();

  private static final ThreadLocal<String> storage = new ThreadLocal<>();

  public void logIn(String user) {
    storage.set(user);
  }

  public void logOut() {
    storage.remove();
  }

  public String get() {
    return storage.get();
  }
}

当前用户.java

完成此配置后,REVINFO 表现在包含一个新字段:用户名

我直接从 Envers 文档中复制了这三个类。访问此 URL 了解更多信息:Hibernate ORM 5.4.33.Final User Guide

例子

在本节中,我们将插入、更新和删除一些数据,并查看 Envers 如何存储更改。

修订版 1:插入

首先,用户“Alice”插入一家公司和两名员工。

    EntityManager em = JPAUtil.getEntityManagerFactory().createEntityManager();
    CurrentUser.INSTANCE.logIn("Alice");

    em.getTransaction().begin();
    Company company = new Company();
    company.setName("E Corp");
    company.setCity("New York City");
    company.setStreet(null);

    Set<Employee> employees = new HashSet<>();

    Employee employee = new Employee();
    employee.setCompany(company);
    employee.setLastName("Spencer");
    employee.setFirstName("Linda");
    employee.setStreet("High Street 123");
    employee.setCity("Newark");
    employees.add(employee);

    employee = new Employee();
    employee.setCompany(company);
    employee.setLastName("Ralbern");
    employee.setFirstName("Michael");
    employee.setStreet("57th Street");
    employee.setCity("New York City");
    employees.add(employee);

    company.setEmployees(employees);

    em.persist(company);
    em.getTransaction().commit();

主.java

请注意,您不必调用任何特殊的 Envers 方法。只需编写标准的JPA(或Hibernate)代码。在后台,Envers 侦听任何更新,并自动将审核信息插入数据库。

Envers 在 REVINFO 表中插入了一行新行,其中包含时间戳和用户名。由于我们在一个事务中插入了三个实体,因此只创建了一个 REVINFO 行。

Envers 还在新公司的COMPANY_AUD中插入了一个新行,REVTYPE 为 0 表示插入操作。此外,Envers 在EMPLOYEE_AUD表中插入了两行新行。因为在插入中,所有属性都已更改,所有_MOD字段都包含值 true。

REVINFO
+----+---------------+----------+
| ID |   TIMESTAMP   | USERNAME |
+----+---------------+----------+
|  1 | 1564997410711 | Alice    |
+----+---------------+----------+

COMPANY_AUD
+----+-----+---------+--------+
| ID | REV | REVTYPE |  NAME  |
+----+-----+---------+--------+
|  1 |   1 |       0 | E Corp |
+----+-----+---------+--------+

EMPLOYEE_AUD
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+
| ID | REV | REVTYPE |      CITY      | CITY_MOD | FIRSTNAME | FIRSTNAME_MOD | LASTNAME | LASTNAME_MOD |      STREET      | STREET_MOD | COMPANY_ID | COMPANY_MOD |
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+
|  1 |   1 |       0 | New York City  |     TRUE | Michael   |          TRUE | Ralbern  |         TRUE | 57th Street      |       TRUE |          1 |        TRUE |
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+
|  2 |   1 |       0 | Newark         |     TRUE | Linda     |          TRUE | Spencer  |         TRUE | High Street 123  |       TRUE |          1 |        TRUE |
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+

修订版 2:更新公司

在下一笔交易中,“Bob”将公司名称从“E Corp”更改为“EEE Corp”。

    CurrentUser.INSTANCE.logIn("Bob");

    em.getTransaction().begin();
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<Company> q = cb.createQuery(Company.class);
    Root<Company> c = q.from(Company.class);
    ParameterExpression<String> p = cb.parameter(String.class);
    q.select(c).where(cb.equal(c.get("name"), p));

    TypedQuery<Company> query1 = em.createQuery(q);
    query1.setParameter(p, "E Corp");

    company = query1.getSingleResult();
    company.setName("EEE Corp");

    em.getTransaction().commit();

主.java

Envers 创建一个新的修订版本,并在COMPANY_AUD表中插入一个新行。REVTYPE = 1 表示更新操作。

REVINFO
+----+---------------+----------+
| ID |   TIMESTAMP   | USERNAME |
+----+---------------+----------+
|  1 | 1564997410711 | Alice    |
+----+---------------+----------+
|  2 | 1564997410849 | Bob      |
+----+---------------+----------+

COMPANY_AUD
+----+-----+---------+----------+
| ID | REV | REVTYPE |   NAME   |
+----+-----+---------+----------+
|  1 |   1 |       0 | E Corp   |
+----+-----+---------+----------+
|  1 |   2 |       1 | EEE Corp |
+----+-----+---------+----------+

修订版 3:新员工

“鲍勃”插入新员工:珍妮特·罗宾逊

    CurrentUser.INSTANCE.logIn("Bob");

    em.getTransaction().begin();
    employee = new Employee();
    employee.setCompany(company);
    employee.setLastName("Robinson");
    employee.setFirstName("Janet");
    employee.setCity("Greenwich");
    employee.setStreet("Walsh Ln 10");
    company.getEmployees().add(employee);
    em.getTransaction().commit();

主.java

REVINFO
+----+---------------+----------+
| ID |   TIMESTAMP   | USERNAME |
+----+---------------+----------+
|  1 | 1564997410711 | Alice    |
+----+---------------+----------+
|  2 | 1564997410849 | Bob      |
+----+---------------+----------+
|  3 | 1564997410858 | Bob      |
+----+---------------+----------+


EMPLOYEE_AUD
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+
| ID | REV | REVTYPE |      CITY      | CITY_MOD | FIRSTNAME | FIRSTNAME_MOD | LASTNAME | LASTNAME_MOD |      STREET      | STREET_MOD | COMPANY_ID | COMPANY_MOD |
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+
|  1 |   1 |       0 | New York City  |     TRUE | Michael   |          TRUE | Ralbern  |         TRUE | 57th Street      |       TRUE |          1 |        TRUE |
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+
|  2 |   1 |       0 | Newark         |     TRUE | Linda     |          TRUE | Spencer  |         TRUE | High Street 123  |       TRUE |          1 |        TRUE |
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+
|  3 |   3 |       0 | Greenwich      |     TRUE | Janet     |          TRUE | Robinson |         TRUE | Walsh Ln 10      |       TRUE |          1 |        TRUE |
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+

Revision 4: Update Employee

"Alice" updates the street and city of the employee Linda Spencer

    CurrentUser.INSTANCE.logIn("Alice");

    em.getTransaction().begin();
    TypedQuery<Employee> query2 = createEmployeeQuery(em, "Linda", "Spencer");
    employee = query2.getSingleResult();
    employee.setStreet("101 W 91st St");
    employee.setCity("New York City");
    em.getTransaction().commit();

Main.java

REVINFO
+----+---------------+----------+
| ID |   TIMESTAMP   | USERNAME |
+----+---------------+----------+
|  1 | 1564997410711 | Alice    |
+----+---------------+----------+
|  2 | 1564997410849 | Bob      |
+----+---------------+----------+
|  3 | 1564997410858 | Bob      |
+----+---------------+----------+
|  4 | 1564997410873 | Alice    |
+----+---------------+----------+

在这里,我们看到只有 CITY_MOD 和 STREET_MOD 设置为 true,因为这是我们在代码中更改的唯一两个属性。

EMPLOYEE_AUD
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+
| ID | REV | REVTYPE |      CITY      | CITY_MOD | FIRSTNAME | FIRSTNAME_MOD | LASTNAME | LASTNAME_MOD |      STREET      | STREET_MOD | COMPANY_ID | COMPANY_MOD |
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+
|  1 |   1 |       0 | New York City  |     TRUE | Michael   |          TRUE | Ralbern  |         TRUE | 57th Street      |       TRUE |          1 |        TRUE |
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+
|  2 |   1 |       0 | Newark         |     TRUE | Linda     |          TRUE | Spencer  |         TRUE | High Street 123  |       TRUE |          1 |        TRUE |
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+
|  3 |   3 |       0 | Greenwich      |     TRUE | Janet     |          TRUE | Robinson |         TRUE | Walsh Ln 10      |       TRUE |          1 |        TRUE |
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+
|  2 |   4 |       1 | New York City  |     TRUE | Linda     |         FALSE | Spencer  |        FALSE | 101 W 91st St    |       TRUE |          1 |       FALSE |
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+

修订版 5:删除员工

爱丽丝删除了员工迈克尔·拉尔伯恩

    CurrentUser.INSTANCE.logIn("Alice");

    em.getTransaction().begin();
    TypedQuery<Employee> query3 = createEmployeeQuery(em, "Michael", "Ralbern");
    employee = query3.getSingleResult();
    employee.getCompany().getEmployees().remove(employee);
    em.remove(employee);
    em.getTransaction().commit();

主.java

REVINFO
+----+---------------+----------+
| ID |   TIMESTAMP   | USERNAME |
+----+---------------+----------+
|  1 | 1564997410711 | Alice    |
+----+---------------+----------+
|  2 | 1564997410849 | Bob      |
+----+---------------+----------+
|  3 | 1564997410858 | Bob      |
+----+---------------+----------+
|  4 | 1564997410873 | Alice    |
+----+---------------+----------+
|  5 | 1564997410892 | Alice    |
+----+---------------+----------+

在这里,我们看到 REVTYPE 2,它表示 DELETE 操作。对于删除操作,所有属性都设置为 NULL。

EMPLOYEE_AUD
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+
| ID | REV | REVTYPE |      CITY      | CITY_MOD | FIRSTNAME | FIRSTNAME_MOD | LASTNAME | LASTNAME_MOD |      STREET      | STREET_MOD | COMPANY_ID | COMPANY_MOD |
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+
|  1 |   1 |       0 | New York City  |     TRUE | Michael   |          TRUE | Ralbern  |         TRUE | 57th Street      |       TRUE |          1 |        TRUE |
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+
|  2 |   1 |       0 | Newark         |     TRUE | Linda     |          TRUE | Spencer  |         TRUE | High Street 123  |       TRUE |          1 |        TRUE |
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+
|  3 |   3 |       0 | Greenwich      |     TRUE | Janet     |          TRUE | Robinson |         TRUE | Walsh Ln 10      |       TRUE |          1 |        TRUE |
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+
|  2 |   4 |       1 | New York City  |     TRUE | Linda     |         FALSE | Spencer  |        FALSE | 101 W 91st St    |       TRUE |          1 |       FALSE |
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+
|  1 |   5 |       2 | NULL           |     TRUE | NULL      |          TRUE | NULL     |         TRUE | NULL             |       TRUE |       NULL |        TRUE |
+----+-----+---------+----------------+----------+-----------+---------------+----------+--------------+------------------+------------+------------+-------------+

查询

存储审核/版本信息是故事的一面,但我们还需要一种方法来在需要时访问此信息。为此,Envers 提供了几种方法来查询审计表。

查询方法的主要入口点是类AuditReader。创建实例,并将实体管理器实例作为参数传递。AuditReaderFactory.get

    EntityManager em = JPAUtil.getEntityManagerFactory().createEntityManager();
    AuditReader reader = AuditReaderFactory.get(em);

主查询.java

getRevision() 方法返回一个修订号列表,在该修订号处修改了实体。该方法需要实体类和实体的主键作为参数。

    List<Number> revisions = reader.getRevisions(Company.class, 1);
    for (Number rev : revisions) {
      System.out.println(rev);

主查询.java

使用getRevisionDate(),我们可以访问修订日期 (REVINFO.时间戳)。

      Date revisionDate = reader.getRevisionDate(rev);
      System.out.println(revisionDate);

主查询.java

要访问修订表中的自定义用户名字段,我们需要调用findRevision() 并将自定义修订实体类的类和修订号作为参数传递。

      CustomRevisionEntity revision = reader.findRevision(CustomRevisionEntity.class,
          rev);
      String username = revision.getUsername();
      System.out.println(username);

主查询.java

使用find(),我们通过主键和给定的修订获得一个实体。

      Company comp = reader.find(Company.class, 1, rev);
      String name = comp.getName();
      String street = comp.getStreet();
      System.out.println(name);
      System.out.println(street);

主查询.java

应用程序打印以下输出。

1
Mon Aug 05 07:46:25 CEST 2019
Alice
E Corp
null
------------------------------------------------
2
Mon Aug 05 07:46:25 CEST 2019
Bob
EEE Corp
null

公司在修订版 1(插入)和修订版 2(更新名称)中进行了更改。请注意,我们从中获取的公司实例的街道属性为 null,因为我们只审核该属性。find()name

您还可以调用未更改给定实体类的修订号。在修订版 5 中,我们删除了一名员工。这就是公司在该修订时的状态。find()find()

    Company comp = reader.find(Company.class, 1, 5);
    String name = comp.getName();
    System.out.println(name); // output: EEE Corp

主查询.java

该库还提供了将 Date 对象而不是修订号作为第三个参数的变体。然后,这将返回该特定日期状态的实体。find()

另一个有用的方法是getRevisionNumberForDate()。此方法返回在给定日期当天或之前创建的最高修订号。

    Calendar cal = Calendar.getInstance();
    Number revNumber = reader.getRevisionNumberForDate(cal.getTime());
    System.out.println(revNumber); // output: 5

主查询.java

审计查询

让我们看一下更高级的查询,即使用 AuditReader.createQuery() 访问的AuditQuery类的所有成员。

forEntitiesAtRevision()查询返回给定类在特定修订版的所有实体。在第一个示例中,我们希望修订版 1 中的所有 Employee 对象。我们取回了在修订版 1 中插入的两个实例。

    AuditQuery query = reader.createQuery().forEntitiesAtRevision(Employee.class, 1);
    query.add(AuditEntity.relatedId("company").eq(1));
    for (Employee e : (List<Employee>) query.getResultList()) {
      System.out.println(e.getId() + ": " + e.getLastName() + " " + e.getFirstName());
    }
    // 1: Ralbern Michael
    // 2: Spencer Linda

主查询.java

所有返回 AuditQuery 实例的查询都可以使用 themethod进一步限制。在上面的示例中,我们仅获取与主键为 1 的公司相关的员工。add()

如果我们使用修订版 2 运行查询,即使我们没有更改修订版 2 中与员工相关的任何内容,我们也会返回相同的两个员工实例。该类像方法一样返回给定修订版中实体的状态,实体在此修订版中是否更改并不重要。find()

    query = reader.createQuery().forEntitiesAtRevision(Employee.class, 2);

主查询.java

    // 1: Ralbern Michael
    // 2: Spencer Linda

当我们查询修订版 5 时,我们看到输出发生了变化。因为我们在修订版 3 中插入了一名新员工,并在修订版 5 中删除了一名员工。

    query = reader.createQuery().forEntitiesAtRevision(Employee.class, 5);

主查询.java

    // 3: Robinson Janet
    // 2: Spencer Linda

为了演示另一个 where 子句,这里有一个例子,我们只希望姓氏等于“Spencer”的实体。

    query = reader.createQuery().forEntitiesAtRevision(Employee.class, 5);
    query.add(AuditEntity.property("lastName").eq("Spencer"));

主查询.java

    // 2: Spencer Linda

您还可以将多个 where 子句与AuditEntity.or()AuditEntity.and()

query.add(AuditEntity.or(AuditEntity.property("lastName").eq("Spencer"), AuditEntity.property("lastName").eq("Robinson")));

forEntitiesAtRevision()默认情况下不返回已删除的实体。您可以通过传递 true 作为第三个参数来更改此设置。

    query = reader.createQuery().forEntitiesAtRevision(Employee.class,
        Employee.class.getName(), 5, true);
    for (Employee e : (List<Employee>) query.getResultList()) {
      System.out.println(e.getId() + ": " + e.getLastName() + " " + e.getFirstName());
    }
    // 3: Robinson Janet
    // 2: Spencer Linda
    // 1: null null

主查询.java

请注意,除已删除实体的主键外,所有属性均为 null。

下一个方法是forEntitiesModifiedAtRevision(),它只返回在给定修订中受影响的实体。与所有 AuditQuery 一样,您可以进一步限制结果query.add()

当我们查询修订版 1 时,我们会返回两名员工,因为我们在此修订版中插入了他们。

    query = reader.createQuery().forEntitiesModifiedAtRevision(Employee.class, 1);
    for (Employee e : (List<Employee>) query.getResultList()) {
      System.out.println(e.getId() + ": " + e.getLastName() + " " + e.getFirstName());
    }
    // 1: Ralbern Michael
    // 2: Spencer Linda

MainQuery.java

When we query revision 2, we get back an empty list, because, in revision 2, we changed the company and didn't change any employee.

    query = reader.createQuery().forEntitiesModifiedAtRevision(Employee.class, 2);
    for (Employee e : (List<Employee>) query.getResultList()) {
      System.out.println(e.getId() + ": " + e.getLastName() + " " + e.getFirstName());
    }
    // empty

MainQuery.java

In revision 5, we deleted an employee, so we get back only this deleted entity.

    query = reader.createQuery().forEntitiesModifiedAtRevision(Employee.class, 5);
    for (Employee e : (List<Employee>) query.getResultList()) {
      System.out.println(e.getId() + ": " + e.getLastName() + " " + e.getFirstName());
    }
    // 1: null null

MainQuery.java

forRevisionsOfEntity() 返回修订列表,在该列表中修改了给定的实体类。结果是一个包含实体类 (0)、修订实体 (1) 和修订类型 (2) 的三元素数组列表

如果将第二个布尔参数设置为 true,则该方法将返回实体类的列表,而不是包含三元素数组的列表。

第三个布尔参数指定查询是否应返回已删除的实体 (true) 或不返回 (false)。

    query = reader.createQuery().forRevisionsOfEntity(Employee.class, false, true);
    // query.add(AuditEntity.id().eq(1));
    List<Object[]> results = query.getResultList();
    for (Object[] result : results) {
      Employee employee = (Employee) result[0];
      CustomRevisionEntity revEntity = (CustomRevisionEntity) result[1];
      RevisionType revType = (RevisionType) result[2];

      System.out.println("Revision     : " + revEntity.getId());
      System.out.println("Revision Date: " + revEntity.getRevisionDate());
      System.out.println("User         : " + revEntity.getUsername());
      System.out.println("Type         : " + revType);
      System.out.println(
          "Employee     : " + employee.getLastName() + " " + employee.getFirstName());

      System.out.println("------------------------------------------------");
    }

主查询.java

上面代码的输出。请注意,修订版 2 未列出,因为我们仅在该特定修订版中更改了公司。

Revision : 1
Revision Date: Mon Aug 05 07:46:25 CEST 2019
User : Alice
Type : ADD
Employee : Ralbern Michael
------------------------------------------------
Revision : 1
Revision Date: Mon Aug 05 07:46:25 CEST 2019
User : Alice
Type : ADD
Employee : Spencer Linda
------------------------------------------------
Revision : 3
Revision Date: Mon Aug 05 07:46:25 CEST 2019
User : Bob
Type : ADD
Employee : Robinson Janet
------------------------------------------------
Revision : 4
Revision Date: Mon Aug 05 07:46:25 CEST 2019
User : Alice
Type : MOD
Employee : Spencer Linda
------------------------------------------------
Revision : 5
Revision Date: Mon Aug 05 07:46:25 CEST 2019
User : Alice
Type : DEL
Employee : null null

另一个有用的条款是,通过它,我们可以将修订限制为仅由一个特定用户的更改引起的修订。AuditEntity.revisionProperty

    query = reader.createQuery().forRevisionsOfEntity(Employee.class, false, true);
    query.add(AuditEntity.revisionProperty("username").eq("Bob"));

主查询.java

“鲍勃”在修订版 3 中只更新了一名员工

Revision : 3
Revision Date: Mon Aug 05 07:46:25 CEST 2019
User : Bob
Type : ADD
Employee : Robinson Janet

我们在此博客文章中介绍的最后一个查询方法是forRevisionsOfEntityWithChanges()。此方法的工作方式与 相同。唯一的区别是此方法返回一个四元素数组:实体类 (0)、修订实体 (1)、修订类型 (2) 以及在此修订 (3) 中更改的一组属性名称。forRevisionsOfEntity()

如果要在应用程序中使用此查询,则必须启用 withModifiedFlag 标志 ()。@Audited(withModifiedFlag = true)

    query = reader.createQuery().forRevisionsOfEntityWithChanges(Employee.class, true);
    results = query.getResultList();
    for (Object[] result : results) {
      Employee employee = (Employee) result[0];
      CustomRevisionEntity revEntity = (CustomRevisionEntity) result[1];
      RevisionType revType = (RevisionType) result[2];
      Set<String> properties = (Set<String>) result[3];

      System.out.println("Revision     : " + revEntity.getId());
      System.out.println("Revision Date: " + revEntity.getRevisionDate());
      System.out.println("User         : " + revEntity.getUsername());
      System.out.println("Type         : " + revType);
      System.out.println("Changed Props: " + properties);
      System.out.println(
          "Employee     : " + employee.getLastName() + " " + employee.getFirstName());

      System.out.println("------------------------------------------------");
    }

主查询.java

请注意,具有已更改属性的集仅包含修订类型为 MOD(更新)的值。

Revision : 1
Revision Date: Mon Aug 05 07:46:25 CEST 2019
User : Alice
Type : ADD
Changed Props: []
Employee : Ralbern Michael
------------------------------------------------
Revision : 1
Revision Date: Mon Aug 05 07:46:25 CEST 2019
User : Alice
Type : ADD
Changed Props: []
Employee : Spencer Linda
------------------------------------------------
Revision : 3
Revision Date: Mon Aug 05 07:46:25 CEST 2019
User : Bob
Type : ADD
Changed Props: []
Employee : Robinson Janet
------------------------------------------------
Revision : 4
Revision Date: Mon Aug 05 07:46:25 CEST 2019
User : Alice
Type : MOD
Changed Props: [city, street]
Employee : Spencer Linda
------------------------------------------------
Revision : 5
Revision Date: Mon Aug 05 07:46:25 CEST 2019
User : Alice
Type : DEL
Changed Props: []
Employee : null null  

我对Envers的概述到此结束。

请参阅官方文档以了解有关Envers:Hibernate ORM 5.4.33.Final User Guide 的更多信息

这篇博文提供的源代码托管在GitHub上:
blog2019/envers at master · ralscha/blog2019 · GitHub

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1. Hibernate 是一种: A. ORM框架 B. MVC框架 C. AOP框架 D. IOC框架 答案:A 2. MyBatis 是一种: A. ORM框架 B. MVC框架 C. AOP框架 D. IOC框架 答案:A 3. MyBatis-Plus 是一种: A. ORM框架 B. MVC框架 C. AOP框架 D. IOC框架 答案:A 4. Hibernate 支持的数据库类型包括: A. MySQL B. Oracle C. Microsoft SQL Server D. 所有主流数据库 答案:D 5. MyBatis 支持的数据库类型包括: A. MySQL B. Oracle C. Microsoft SQL Server D. 所有主流数据库 答案:D 6. MyBatis-Plus 支持的数据库类型包括: A. MySQL B. Oracle C. Microsoft SQL Server D. 所有主流数据库 答案:D 7. Hibernate 的核心特性包括: A. ORM映射 B. 缓存管理 C. 事务管理 D. 所有选项都是 答案:D 8. MyBatis 的核心特性包括: A. SQL映射 B. 缓存管理 C. 事务管理 D. 所有选项都是 答案:D 9. MyBatis-Plus 的核心特性包括: A. 自动代码生成 B. 缓存管理 C. 事务管理 D. 所有选项都是 答案:D 10. Hibernate 的二级缓存机制是基于: A. 内存 B. Redis C. Memcached D. Ehcache 答案:D 11. MyBatis 的二级缓存机制是基于: A. 内存 B. Redis C. Memcached D. Ehcache 答案:D 12. MyBatis-Plus 的二级缓存机制是基于: A. 内存 B. Redis C. Memcached D. Ehcache 答案:D 13. Hibernate 的一级缓存机制是基于: A. 内存 B. Redis C. Memcached D. Ehcache 答案:A 14. MyBatis 的一级缓存机制是基于: A. 内存 B. Redis C. Memcached D. Ehcache 答案:A 15. MyBatis-Plus 的一级缓存机制是基于: A. 内存 B. Redis C. Memcached D. Ehcache 答案:A 16. Hibernate 的事务管理支持: A. 编程式事务管理 B. 声明式事务管理 C. 两种都支持 D. 都不支持 答案:C 17. MyBatis 的事务管理支持: A. 编程式事务管理 B. 声明式事务管理 C. 两种都支持 D. 都不支持 答案:A 18. MyBatis-Plus 的事务管理支持: A. 编程式事务管理 B. 声明式事务管理 C. 两种都支持 D. 都不支持 答案:A 19. Hibernate 的主键生成策略包括: A. 自增长 B. UUID C. 序列 D. 所有选项都是 答案:D 20. MyBatis 的主键生成策略包括: A. 自增长 B. UUID C. 序列 D. 所有选项都是 答案:D 21. MyBatis-Plus 的主键生成策略包括: A. 自增长 B. UUID C. 序列 D. 所有选项都是 答案:D 22. Hibernate 的延迟加载机制是通过: A. 代理模式 B. 动态代理模式 C. CGLIB D. 所有选项都是 答案:A 23. MyBatis 的延迟加载机制是通过: A. 代理模式 B. 动态代理模式 C. CGLIB D. 所有选项都是 答案:A 24. MyBatis-Plus 的延迟加载机制是通过: A. 代理模式 B. 动态代理模式 C. CGLIB D. 所有选项都是 答案:A 25. Hibernate 的动态 SQL 生成是通过: A. HQL B. Criteria API C. SQL D. 所有选项都是 答案:B 26. MyBatis 的动态 SQL 生成是通过: A. HQL B. Criteria API C. SQL D. 所有选项都是 答案:C 27. MyBatis-Plus 的动态 SQL 生成是通过: A. HQL B. Criteria API C. SQL D. 所有选项都是 答案:B 28. Hibernate 的优点包括: A. 易于学习 B. 代码量少 C. 易于维护 D. 所有选项都是 答案:C 29. MyBatis 的优点包括: A. 易于学习 B. 代码量少 C. 易于维护 D. 所有选项都是 答案:A 30. MyBatis-Plus 的优点包括: A. 易于学习 B. 代码量少 C. 易于维护 D. 所有选项都是 答案:B 31. Hibernate 的缺点包括: A. 性能问题 B. 学习曲线陡峭 C. 代码量大 D. 所有选项都是 答案:A 32. MyBatis 的缺点包括: A. 性能问题 B. 学习曲线陡峭 C. 代码量大 D. 所有选项都是 答案:B 33. MyBatis-Plus 的缺点包括: A. 性能问题 B. 学习曲线陡峭 C. 代码量大 D. 所有选项都是 答案:C 34. 在 Hibernate 中,如何配置 ORM 映射? A. 使用注解 B. 使用 XML 文件 C. 两种都可 D. 都不可 答案:C 35. 在 MyBatis 中,如何配置 SQL 映射? A. 使用注解 B. 使用 XML 文件 C. 两种都可 D. 都不可 答案:B 36. 在 MyBatis-Plus 中,如何配置 SQL 映射? A. 使用注解 B. 使用 XML 文件 C. 两种都可 D. 都不可 答案:C 37. 在 Hibernate 中,如何进行复杂查询? A. 使用 HQL B. 使用 SQL C. 使用 Criteria API D. 所有选项都是 答案:D 38. 在 MyBatis 中,如何进行复杂查询? A. 使用 HQL B. 使用 SQL C. 使用 Criteria API D. 所有选项都是 答案:B 39. 在 MyBatis-Plus 中,如何进行复杂查询? A. 使用 HQL B. 使用 SQL C. 使用 Criteria API D. 所有选项都是 答案:C 40. 在 Hibernate 中,如何配置一对多关系? A. 使用 @OneToMany 注解 B. 在 XML 文件中配置 C. 两种都可 D. 都不可 答案:C 41. 在 MyBatis 中,如何配置一对多关系? A. 使用 @OneToMany 注解 B. 在 XML 文件中配置 C. 两种都可 D. 都不可 答案:B 42. 在 MyBatis-Plus 中,如何配置一对多关系? A. 使用 @OneToMany 注解 B. 在 XML 文件中配置 C. 两种都可 D. 都不可 答案:A 43. 在 Hibernate 中,如何配置多对多关系? A. 使用 @ManyToMany 注解 B. 在 XML 文件中配置 C. 两种都可 D. 都不可 答案:C 44. 在 MyBatis 中,如何配置多对多关系? A. 使用 @ManyToMany 注解 B. 在 XML 文件中配置 C. 两种都可 D. 都不可 答案:B 45. 在 MyBatis-Plus 中,如何配置多对多关系? A. 使用 @ManyToMany 注解 B. 在 XML 文件中配置 C. 两种都可 D. 都不可 答案:A 46. 在 Hibernate 中,如何进行分页查询? A. 使用 HQL B. 使用 SQL C. 使用 Criteria API D. 所有选项都是 答案:D 47. 在 MyBatis 中,如何进行分页查询? A. 使用 HQL B. 使用 SQL C. 使用 Criteria API D. 所有选项都是 答案:B 48. 在 MyBatis-Plus 中,如何进行分页查询? A. 使用 HQL B. 使用 SQL C. 使用 Criteria API D. 所有选项都是 答案:C 49. 在 Hibernate 中,如何配置多数据源? A. 使用 Spring Data JPA B. 使用 Hibernate Envers C. 使用 Spring Boot D. 所有选项都不是 答案:C 50. 在 MyBatis 中,如何配置多数据源? A. 使用 Spring Data JPA B. 使用 MyBatis Generator C. 使用 Spring Boot D. 所有选项都不是 答案:C

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值