编程

Hibernate Envers – 入门导引

754 2024-06-21 15:09:00

许多业务应用都需要一个审计日志来记录对托管数据执行的所有更改。有很多不同的选项可以实现这样的日志。其中之一是 Hibernate Envers。只需要一些注释就可以记录审核表中的所有更改,Envers 还提供了一个强大的 API 来从审核日志中提取信息。

本文将向展示如何将 Hibernate Envers 添加到项目中,激活实体的审核,并从日志中检索不同的信息。

项目安装

将 Hibernate Envers 添加到现有应用程序非常容易。只需将 hibernate-envers.jar 文件添加到类路径中。如果使用的是 maven,可以使用以下 maven 依赖项来实现这一点。

<dependency>

  <groupId>org.hibernate</groupId>

  <artifactId>hibernate-envers</artifactId>

  <version>5.2.5.Final</version>

</dependency>

接下来需要做的是设置审核表。如果使用自动模式生成功能,Hibernate 本身就可以做到这一点。但我不建议这样做。你可以使用该功能来创建数据库脚本,但不应在未对其进行审查和改进的情况下将其部署到生产环境中。

因此,以下是你需要在数据库设置或迁移脚本中创建的表:

REVINFO

此表存储修订版本信息。默认情况下,Hibernate 只将修订号持久化为整型,将创建时间戳保留为 long 类型。

CREATE TABLE revinfo

(

rev integer NOT NULL,

revtstmp bigint,

CONSTRAINT revinfo_pkey PRIMARY KEY (rev)

)

我将在后面的博文中展示如何向该表添加更多信息,就像创建修订版的用户一样

每个实体的一个审计表

你还需要为每个要审核的实体创建一个审核表。默认情况下,Hibernate 会将 “_AUD” 后缀添加到要审核实体的表名中。你可以使用 @AuditTable 注释或通过在配置中配置不同的前缀或后缀来定义不同的表名。

每个审核表都包含原始实体的主键、所有审核字段、修订号和修订类型。修订号必须与修订表中的记录相匹配,并与 id 字段一起使用以创建联合主键。修订类型保留在给定修订中对实体执行的操作类型。Envers 使用整数值 0、1 和 2 来存储添加、更新或删除实体的信息。

下面的代码片段显示了一个审核表的示例。它存储 Author 实体的审核信息,并跟踪对 firstname 和 lastname 属性的所有更改。

CREATE TABLE author_aud

(

id bigint NOT NULL,

rev integer NOT NULL,

revtype smallint,

firstname character varying(255),

lastname character varying(255),

CONSTRAINT author_aud_pkey PRIMARY KEY (id, rev),

CONSTRAINT author_aud_revinfo FOREIGN KEY (rev)

REFERENCES revinfo (rev) MATCH SIMPLE

ON UPDATE NO ACTION ON DELETE NO ACTION

)

这就是将 Hibernate Envers 添加到应用所需做的全部工作。现在,你可以告诉 Hibernate 要审计哪些实体。

审计实体

如果要审核实体的所有修改,则必须使用 @Audited 对其进行注释。这告诉 Hibernate Envers 审计所有创建、更新和删除操作的所有属性的值。当然,你可以应用额外的注释和配置,以使审核适应你的需要。我将在下面的一篇帖子中更详细地解释这一点。

@Entity

@Audited

public class Author implements Serializable { … }

@Audited 注释实体后,Hibernate 将为每个事务创建一个新的修订,并记录审核表中的所有更改。以下代码片段显示了基本的持久化和更新操作以及 Hibernate 执行的 SQL 语句。你无需以任何方式调整业务代码。

EntityManager em = emf.createEntityManager();

em.getTransaction().begin();

Author a = new Author();

a.setFirstName(“Thorben”);

a.setLastName(“Janssen”);

em.persist(a);

Book b = new Book();

b.setTitle(“Hibernate Tips”);

b.getAuthors().add(a);

a.getBooks().add(b);

em.persist(b);

em.getTransaction().commit();

em.close();
10:50:30,950 DEBUG SQL:92 -

    select

        nextval ('hibernate_sequence')

10:50:30,989 DEBUG SQL:92 -

    select

        nextval ('hibernate_sequence')

10:50:31,013 DEBUG SQL:92 -

    insert

    into

        Author

        (firstName, lastName, version, id)

    values

        (?, ?, ?, ?)

10:50:31,024 DEBUG SQL:92 -

    insert

    into

        Book

        (publisherid, publishingDate, title, version, id)

    values

        (?, ?, ?, ?, ?)

10:50:31,029 DEBUG SQL:92 -

    insert

    into

        BookAuthor

        (bookId, authorId)

    values

        (?, ?)

10:50:31,042 DEBUG SQL:92 -

    select

        nextval ('hibernate_sequence')

10:50:31,046 DEBUG SQL:92 -

    insert

    into

        REVINFO

        (REVTSTMP, REV)

    values

        (?, ?)

10:50:31,048 DEBUG SQL:92 -

    insert

    into

        Author_AUD

        (REVTYPE, firstName, lastName, id, REV)

    values

        (?, ?, ?, ?, ?)

10:50:31,051 DEBUG SQL:92 -

    insert

    into

        Book_AUD

        (REVTYPE, publishingDate, title, publisherid, id, REV)

    values

        (?, ?, ?, ?, ?, ?)

10:50:31,054 DEBUG SQL:92 -

    insert

    into

        BookAuthor_AUD

        (REVTYPE, REV, bookId, authorId)

    values

        (?, ?, ?, ?)
Book b = em.find(Book.class, b.getId());

b.setTitle(“Hibernate Tips – 64 Tips for your day to day work”)
10:49:29,465 DEBUG SQL:92 -

    select

        book0_.id as id1_2_0_,

        book0_.publisherid as publishe5_2_0_,

        book0_.publishingDate as publishi2_2_0_,

        book0_.title as title3_2_0_,

        book0_.version as version4_2_0_,

        publisher1_.id as id1_6_1_,

        publisher1_.name as name2_6_1_,

        publisher1_.version as version3_6_1_

    from

        Book book0_

    left outer join

        Publisher publisher1_

            on book0_.publisherid=publisher1_.id

    where

        book0_.id=?

10:49:29,483 DEBUG SQL:92 -

    update

        Book

    set

        publisherid=?,

        publishingDate=?,

        title=?,

        version=?

    where

        id=?

        and version=?

10:49:29,487 DEBUG SQL:92 -

    select

        nextval ('hibernate_sequence')

10:49:29,489 DEBUG SQL:92 -

    insert

    into

        REVINFO

        (REVTSTMP, REV)

    values

        (?, ?)

10:49:29,491 DEBUG SQL:92 -

    insert

    into

        Book_AUD

        (REVTYPE, publishingDate, title, publisherid, id, REV)

    values

        (?, ?, ?, ?, ?, ?)

检索基础审计信息

Hibernate Envers 提供了一个可扩展的 Query API,你可以使用它从审核日志中提取所需的信息。在这篇文章中,我只展示如何检索实体的所有修订,以及如何检索在某个时间点处于活动状态的修订。这只是两个基本的用例,你可以使用Query API 做更多的事情。

获取实体的所有版本信息

访问审核信息需要做的第一件事是通过 AuditReaderFactory 创建 AuditReader。你可以在以下代码的第一行中看到一个示例。我使用 EntityManager 的当前实例调用 AuditReaderFactorget 方法。

AuditReadergetRevisions 方法返回给定实体的所有版本号。你可以使用这些数字来获取一个实体,该实体具有在给定修订时具有的所有属性。代码的第 5 行中这样做了这样的操作。我遍历修订号列表,并为每个修订号调用 find 方法,以获得在给定修订时处于活动状态的 Book 实体。

AuditReader auditReader = AuditReaderFactory.get(em);

List revisionNumbers = auditReader.getRevisions(Book.class, b.getId());

for (Number rev : revisionNumbers) {

    Book auditedBook = auditReader.find(Book.class, b.getId(), rev);

    log.info(“Book [“+auditedBook+”] at revision [“+rev+”].”);

}

正如你在日志消息中看到的,Hibernate 执行 SQL 查询以获取给定实体的修订号。find 方法的调用会触发另一个 SQL 查询,该查询将返回针对给定修订号激活的审核表中的记录。

10:51:52,378 DEBUG SQL:92 -

    select

        book_aud0_.REV as col_0_0_

    from

        Book_AUD book_aud0_ cross

    join

        REVINFO defaultrev1_

    where

        book_aud0_.id=?

        and book_aud0_.REV=defaultrev1_.REV

    order by

        book_aud0_.REV asc

10:51:52,407 DEBUG SQL:92 -

    select

        book_aud0_.id as id1_3_,

        book_aud0_.REV as REV2_3_,

        book_aud0_.REVTYPE as REVTYPE3_3_,

        book_aud0_.publishingDate as publishi4_3_,

        book_aud0_.title as title5_3_,

        book_aud0_.publisherid as publishe6_3_

    from

        Book_AUD book_aud0_

    where

        book_aud0_.REV=(

            select

                max(book_aud1_.REV)

            from

                Book_AUD book_aud1_

            where

                book_aud1_.REV<=?

                and book_aud0_.id=book_aud1_.id

        )

        and book_aud0_.REVTYPE<>?

        and book_aud0_.id=?

10:51:52,418  INFO TestEnvers:118 - Book [Book title: Hibernate Tips] at revision [2].

10:51:52,419 DEBUG SQL:92 -

    select

        book_aud0_.id as id1_3_,

        book_aud0_.REV as REV2_3_,

        book_aud0_.REVTYPE as REVTYPE3_3_,

        book_aud0_.publishingDate as publishi4_3_,

        book_aud0_.title as title5_3_,

        book_aud0_.publisherid as publishe6_3_

    from

        Book_AUD book_aud0_

    where

        book_aud0_.REV=(

            select

                max(book_aud1_.REV)

            from

                Book_AUD book_aud1_

            where

                book_aud1_.REV<=?

                and book_aud0_.id=book_aud1_.id

        )

        and book_aud0_.REVTYPE<>?

        and book_aud0_.id=?

10:51:52,421  INFO TestEnvers:118 - Book [Book title: Hibernate Tips - 64 Tips for your day to day work] at revision [3].

在给定日期获取活动版本

如果只想获得一个在给定时间处于活动状态的实体,可以调用 AuditReaderfind 方法并提供 java.util.Date 而非版本号。你可以在下面的代码中看到一个示例。

AuditReader auditReader = AuditReaderFactory.get(em);

Book auditedBook = auditReader.find(Book.class, b.getId(), created);

log.info(“Book [“+auditedBook+”] at [“+created+”].”);

然后,Hibernate Envers 将执行 SQL 查询以获取在给定时间处于活动状态的修订号,并执行额外的查询以从审核表中选择记录。

10:52:52,067 DEBUG SQL:92 -

    select

        max(defaultrev0_.REV) as col_0_0_

    from

        REVINFO defaultrev0_

    where

        defaultrev0_.REVTSTMP<=?

10:52:52,117 DEBUG SQL:92 -

    select

        book_aud0_.id as id1_3_,

        book_aud0_.REV as REV2_3_,

        book_aud0_.REVTYPE as REVTYPE3_3_,

        book_aud0_.publishingDate as publishi4_3_,

        book_aud0_.title as title5_3_,

        book_aud0_.publisherid as publishe6_3_

    from

        Book_AUD book_aud0_

    where

        book_aud0_.REV=(

            select

                max(book_aud1_.REV)

            from

                Book_AUD book_aud1_

            where

                book_aud1_.REV<=?

                and book_aud0_.id=book_aud1_.id

        )

        and book_aud0_.REVTYPE<>?

        and book_aud0_.id=?

总结

Hibernate Envers 提供了一个强大且易用的 API 来编写和读取审计信息。只需将 hibernate-envers.jar 文件添加到应用的类路径中,并使用 @Audited 注释实体。然后,Hibernate 将为每个事务创建一个新的修订版,并在审计表中为对已审计实体执行的每个创建、更新或删除操作创建一条新记录。

这篇文章只提供了一个简短的 Hibernate Envers 介绍。后续的博文中,我将向你展示更多关于查询 API 的内容,以及如何自定义审核日志。