Auditing with JPA, Hibernate, and Spring Data JPA


1.概述

在ORM的上下文中,数据库审计意味着跟踪和记录与持久实体相关的事件,或者仅仅是实体版本控制。受SQL触发器的启发,事件是对实体的插入,更新和删除操作。数据库审计的好处类似于源版本控制提供的好处。

我们将演示将审计引入应用程序的三种方法。首先,我们将使用标准JPA实现它。接下来,我们将看两个提供自己的审计功能的JPA扩展:一个由Hibernate提供,另一个由Spring Data提供。

以下是将在此示例中使用的示例相关实体Bar和Foo:

2.使用JPA进行审核

JPA没有明确包含审计API,但可以使用实体生命周期事件来实现功能。

2.1。@PrePersist, @PreUpdate和@PreRemove

在JPA Entity类中,可以将方法指定为将在特定实体生命周期事件期间调用的回调。由于我们感兴趣的是,相应的DML操作之前执行的回调,有@PrePersist,@PreUpdate和@PreRemove回调的注解可为我们的宗旨:

@Entity
public class Bar {

    @PrePersist
    public void onPrePersist() { ... }

    @PreUpdate
    public void onPreUpdate() { ... }

    @PreRemove
    public void onPreRemove() { ... }

}

内部回调方法应始终返回void并且不带参数。它们可以具有任何名称和任何访问级别,但不应该是静态的。

请注意,JPA 中的@Version注释与我们的主题并不严格相关 - 它与乐观锁定相关,而不是与审计数据相关。

2.2。实现回调方法

但是,这种方法存在重大限制。如JPA 2规范(JSR 317)中所述:

In general, the lifecycle method of a portable application should not invoke EntityManager or Query operations, access other entity instances, or modify relationships within the same persistence context. A lifecycle callback method may modify the non-relationship state of the entity on which it is invoked.

在没有审计框架的情况下,我们必须手动维护数据库架构和域模型。对于我们的简单用例,让我们向实体添加两个新属性,因为我们只能管理“实体的非关系状态”。一个操作属性将存储执行的操作的名称和时间戳属性是操作的时间戳:

@Entity
public class Bar {

    //...

    @Column(name = "operation")
    private String operation;

    @Column(name = "timestamp")
    private long timestamp;

    //...

    // standard setters and getters for the new properties

    //...

    @PrePersist
    public void onPrePersist() {
        audit("INSERT");
    }

    @PreUpdate
    public void onPreUpdate() {
        audit("UPDATE");
    }

    @PreRemove
    public void onPreRemove() {
        audit("DELETE");
    }

    private void audit(String operation) {
        setOperation(operation);
        setTimestamp((new Date()).getTime());
    }

}

如果需要将此类审核添加到多个类,则可以使用@EntityListeners来集中代码。例如:

@EntityListeners(AuditListener.class)
@Entity
public class Bar { ... }
public class AuditListener {

    @PrePersist
    @PreUpdate
    @PreRemove
    private void beforeAnyOperation(Object object) { ... }

}

3. Hibernate Envers

使用Hibernate,我们可以使用Interceptor和EventListeners以及数据库触发器来完成审计。但是ORM框架提供了Envers,这是一个实现持久化类的审计和版本控制的模块。

3.1。Envers入门

要设置Envers,需要将hibernate-envers JAR 添加到类路径中:

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-envers</artifactId>
    <version>${hibernate.version}</version>
</dependency>

然后只需在@Entity(审核整个实体)或特定的@Column上添加@Audited注释(如果您只需要审核特定属性):

@Entity
@Audited
public class Bar { ... }

请注意,Bar与Foo有一对多的关系。在这种情况下,我们要么通过在Foo上添加@Audited或在Bar的关系属性上设置@NotAudited来审核Foo:

@OneToMany(mappedBy = "bar")
@NotAudited
private Set<Foo> fooSet;

3.2。创建审核日志表

有几种方法可以创建审计表:

设置hibernate.hbm2ddl.auto来创建,创建 - 删除或更新,这样Envers就可以自动创建它们 使用o rg.hibernate.tool.EnversSchemaGenerator以编程方式导出完整的数据库模式 使用Ant任务生成适当的DDL语句 使用Maven插件从映射(例如Juplo)生成数据库模式以导出Envers模式(与Hibernate 4及更高版本一起使用) 我们将走第一条路线,因为它是最直接的,但要注意使用hibernate.hbm2ddl.auto在生产中是不安全的。

在我们的例子中,bar_AUD和foo_AUD(如果你将Foo设置为@Audited)表应该自动生成。审计表从实体表中复制所有审计字段,其中包含两个字段REVTYPE(值为:“0”表示添加,“1”表示更新,“2”表示删除实体)和REV。

除此之外,默认会生成一个名为REVINFO的额外表,它包括两个重要的字段REV和REVTSTMP,并记录每个修订的时间戳。正如你猜测的那样,bar_AUD.REV和foo_AUD.REV实际上是REVINFO.REV的外键。

3.3。配置Envers

您可以像任何其他Hibernate属性一样配置Envers属性。

例如,让我们将审计表后缀(默认为 _AUD )更改为“ _AUDIT_LOG ”。以下是如何设置相应属性org.hibernate.envers.audit_table_suffix的值:

Properties hibernateProperties = new Properties();
hibernateProperties.setProperty(
  "org.hibernate.envers.audit_table_suffix", "_AUDIT_LOG");
sessionFactory.setHibernateProperties(hibernateProperties);

可以在Envers文档中找到可用属性的完整列表。

3.4。访问实体历史记录

您可以通过类似于通过Hibernate条件API查询数据的方式查询历史数据。实体的审核历史记录可以使用被访问AuditReader接口,其可与一个开放而获得的EntityManager或会话经由AuditReaderFactory:

AuditReader reader = AuditReaderFactory.get(session);

Envers提供AuditQueryCreator(由AuditReader.createQuery()返回)以创建特定于审计的查询。以下行将返回在修订版#2中修改的所有Bar实例(其中bar_AUDIT_LOG.REV = 2):

AuditQuery query = reader.createQuery()
  .forEntitiesAtRevision(Bar.class, 2)

以下是查询Bar的修订版的方法,即它将获得所有审计状态中所有Bar实例的列表:

AuditQuery query = reader.createQuery()
  .forRevisionsOfEntity(Bar.class, true, true);

如果第二个参数为false,则结果与REVINFO表连接,否则,仅返回实体实例。最后一个参数指定是否返回已删除的Bar实例。

然后,您可以使用AuditEntity工厂类指定约束:

query.addOrder(AuditEntity.revisionNumber().desc());

4. Spring Data JPA

Spring Data JPA是一个通过在JPA提供程序的顶部添加额外的抽象层来扩展JPA的框架。该层允许通过扩展Sp​​ring JPA存储库接口来支持创建JPA存储库。

出于我们的目的,您可以扩展CrudRepository <T,ID extends Serializable>,用于通用CRUD操作的接口。一旦您创建了存储库并将其注入另一个组件,Spring Data就会自动提供实现,您就可以添加审计功能了。

4.1。启用JPA审核

首先,我们希望通过注释配置启用审核。为此,只需在@Configuration类上添加@EnableJpaAuditing:

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories
@EnableJpaAuditing
public class PersistenceConfig { ... }

4.2。添加Spring的实体回调监听器

我们已经知道,JPA提供@EntityListeners注释来指定回调侦听器类。Spring Data提供了自己的JPA实体监听器类:AuditingEntityListener。所以让我们为Bar实体指定监听器:

@Entity
@EntityListeners(AuditingEntityListener.class)
public class Bar { ... }

现在,监听器将在持久化和更新Bar实体时捕获审计信息。

4.3。跟踪创建的和上次修改的日期

接下来,我们将添加两个新属性,用于将创建和最后修改日期存储到Bar实体。这些属性由@CreatedDate和@LastModifiedDate注释相应地注释,并且它们的值自动设置:

@Entity
@EntityListeners(AuditingEntityListener.class)
public class Bar {

    //...

    @Column(name = "created_date", nullable = false, updatable = false)
    @CreatedDate
    private long createdDate;

    @Column(name = "modified_date")
    @LastModifiedDate
    private long modifiedDate;

    //...

}

通常,您可以将属性移动到基类(由@MappedSuperClass注释),该基类将由所有审计实体进行扩展。在我们的示例中,为简单起见,我们将它们直接添加到Bar。

4.4。使用Spring Security审核更改的作者

如果您的应用使用Spring Security,您不仅可以跟踪更改的时间,还可以跟踪制作者:

@Entity
@EntityListeners(AuditingEntityListener.class)
public class Bar {

    //...

    @Column(name = "created_by")
    @CreatedBy
    private String createdBy;

    @Column(name = "modified_by")
    @LastModifiedBy
    private String modifiedBy;

    //...

}

使用@CreatedBy和@LastModifiedBy注释的列将填充创建或上次修改实体的主体的名称。该信息从SecurityContext的Authentication实例中提取。如果要自定义设置为带注释字段的值,可以实现AuditorAware 接口:

public class AuditorAwareImpl implements AuditorAware<String> {

    @Override
    public String getCurrentAuditor() {
        // your custom logic
    }

}

为了配置该应用使用AuditorAwareImpl查找当前主体,申报的豆AuditorAware用实例来初始化类型AuditorAwareImpl并指定bean的名称作为auditorAwareRef参数的值@EnableJpaAuditing:

@EnableJpaAuditing(auditorAwareRef="auditorProvider")
public class PersistenceConfig {

    //...

    @Bean
    AuditorAware<String> auditorProvider() {
        return new AuditorAwareImpl();
    }

    //...

}