JPA、Hibernate 和 Spring Data JPA 中的数据库审计

1、概览

就 ORM 而言,数据库审计指的是跟踪和记录与实体相关的事件,或者简单地说是实体版本管理。受 SQL 触发器的启发,这些事件是对实体的插入、更新和删除操作。数据库审计的好处类似于源代码版本控制。

本文将带你了解在应用中使用审计的三种方法。首先介绍来自于 JPA 标准的审计实现、然后再介绍由 Hibernate 和 Spring Data 分别提供的审计的扩展实现。

下面是本文中使用的相关实体 BarFoo 示例:

Bar 和 Foo 实体关系图

2、JPA 审计

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

2.1、@PrePersist、@PreUpdate 和 @PreRemove

在 JPA 实体类中,我们可以使用 @PrePersist@PreUpdate@PreRemove 注解指定一个方法作为回调,以在相应的 DML 操作前执行它。

@Entity
public class Bar {
      
    @PrePersist
    public void onPrePersist() { ... }
      
    @PreUpdate
    public void onPreUpdate() { ... }
      
    @PreRemove
    public void onPreRemove() { ... }
      
}

实体内部的回调方法应是非 static 的,返回类型为 void,且不带参数。访问权限任意。

注意,JPA 中的 @Version 注解与本文的主题没有关系,它是一种乐观锁,与审计数据无关。

2.2、实现回调方法

不过,这种方法有一个很大的限制。正如 JPA 2 规范(JSR 317)所述:

一般来说,可移植应用的生命周期方法不应在同一持久化上下文(Persistence Context)中调用 EntityManagerQuery 操作、访问其他实体实例或修改关系。生命周期回调方法可修改被调用实体的非关联状态。

在没有审计框架的情况下,我们必须手动维护数据库 Schema 和 Domain Model。

在本例中,我们为实体添加两个新属性(只能管理 “实体的非关联状态”)。

  • operation 属性用于存储执行操作的名称。
  • timestamp 属性用于存储执行操作的时间戳。
@Entity
public class Bar {
     
    //...
     
    @Column(name = "operation")
    private String operation;
     
    @Column(name = "timestamp")
    private long timestamp;
     
    //...
     
    // get /set 方法省略
     
    //...
     
    @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)和事件监听器(EventListener)以及数据库触发器来完成审计。

然而,ORM 框架提供了 Envers 模块,它可以实现实体类的审计和版本控制。

3.1、Envers 入门

要使用 Envers,需要在项目中添加 hibernate-envers 依赖:

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

然后,在 @Entity 上(审核整个实体)或在特定的 @Column 上(只审计特定属性)添加 @Audited 注解:

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

注意,BarFoo 是一对多的关系。在这种情况下,我们要么在 Foo 上添加 @Audited 来审计 Foo,要么在 Bar 的关系属性上设置 @NotAudited

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

3.2、创建审计日志表

创建审计表有几种方法:

  • hibernate.hbm2ddl.auto 设置为 createcreate-dropupdate,以便 Envers 自动创建它们。
  • 使用 org.hibernate.tool.EnversSchemaGenerator 以编程式导出完整的数据库 Schema。
  • 设置 Ant task 以生成适当的 DDL 语句。
  • 使用 Maven 插件从映射(如 Juplo)生成数据库 Schema,以导出 Envers Schema(适用于 Hibernate 4 及更高版本)

这里采用第一种方法,因为这是最直接的方法,但要注意,在生产中使用 hibernate.hbm2ddl.auto 并不安全。

在本例中,bar_AUDfoo_AUD(如果将 Foo 也设置为 @Audited)表会自动生成。审计表会从实体表中复制所有需要审计的字段,并包含两个额外的字段 REVTYPE(值为 0 表示添加,1 表示更新,2 表示更新)和 REV

除此之外,默认情况下还会额外生成一个名为 REVINFO 的表。它包括两个重要字段:REVREVTSTMP,并记录每次修订的时间戳。我们其实可以猜到,bar_AUD.REVfoo_AUD.REV 实际上是 REVINFO.REV 的外键。

3.3、配置 Envers

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

例如,通过 org.hibernate.envers.audit_table_suffix 属性把审计表的后缀(默认为 _AUD)改为 _AUDIT_LOG

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

完整可用的属性列表,请参阅 Envers 文档

3.4、查询实体历史的记录

我们可以通过类似于使用 Hibernate Criteria API 查询数据的方式来查询历史数据。

使用 AuditReader 接口访问实体的审计历史记录,通过 AuditReaderFactory 从打开的 EntityManagerSession 中获取该接口:

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 Provider 之上添加了一个额外的抽象层。该层支持通过继承 Spring JPA Repository 接口来创建 JPA Repository。

我们可以继承 CrudRepository<T, ID extends Serializable>,这是用于通用 CRUD 操作的接口。只要我们创建了 Repository 并将其注入另一个组件,Spring Data 就会自动提供实现,我们就可以添加审计功能了。

4.1、启用 JPA 审计

首先,需要通过注解配置启用审计。在 @Configuration 类中添加 @EnableJpaAuditing

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories
@EnableJpaAuditing      // 启用 JPA 审计
public class PersistenceConfig { ... }

4.2、 添加 Spring 实体回调监听器

如上所述,JPA 提供了 @EntityListeners 注解来指定回调监听器类。

Spring Data 提供了自己的 JPA 实体监听器类 AuditingEntityListener

Bar 实体指定 Spring 的实体回调监听器:

@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;
    
    //...
    
}

一般来说,应该将这些审计属性移至一个 Base 类(用 @MappedSuperClass 进行注解),所有需要审计的实体直接继承该 Base 类。在本例中,为了简单起见,直接将它们添加到了 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 注解的列将填充为创建或最后修改实体的用户的名称。

该信息来自 SecurityContextAuthentication 实例。如果想要自定义设置注解字段的值,那么可以实现 AuditorAware<t> 接口:

public class AuditorAwareImpl implements AuditorAware<String> {
 
    @Override
    public String getCurrentAuditor() {
        // 自定义逻辑
    }

}

要配置应用使用 AuditorAwareImpl 查找当前 Principal(认证用户),需要声明一个 AuditorAware 类型的 Bean,并用 AuditorAwareImpl 实例进行初始化,然后将该 Bean 的名称指定为 @EnableJpaAuditingauditorAwareRef 参数的值:

@EnableJpaAuditing(auditorAwareRef="auditorProvider")
public class PersistenceConfig {
    
    //...
    
    @Bean
    AuditorAware<String> auditorProvider() {
        return new AuditorAwareImpl();
    }
    
    //...
    
}

5、总结

本文介绍了三种实现审计功能的方法:

  • 纯粹的 JPA 方法是最基本的方法,它包括使用生命周期回调。然而,我们只允许修改实体的非关联状态。这使得 @PreRemove 回调对我们的目的无用,因为方法中进行的任何设置都将随实体一起被删除。
  • Envers 是 Hibernate 提供的成熟的审计模块。它具有高度可配置性,并且不会出现纯 JPA 实现的缺陷。因此,它允许我们对删除操作进行审计,因为它会将日志记录到实体表以外的其他表中。
  • Spring Data JPA 方法对 JPA 回调进行了抽象,并为审计属性提供了方便的注解。它还可以与 Spring Security 集成。缺点是它继承了 JPA 方法的相同缺陷,因此无法对删除操作进行审计。

你可以根据你的需求,选择合适的方法。


Ref:https://www.baeldung.com/database-auditing-jpa