Contents

JPA,Hibernate和Spring Data JPA进行审核

1. 概述

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

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

以下是我们将在此示例中使用的示例相关实体BarFoo

/uploads/database_auditing_jpa/1.png

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,并且不带任何参数。它们可以有任何名称和任何访问级别,但不应该是static。 请注意,JPA 中的*@Version*注释与我们的主题并不严格相关;它与乐观锁定有关,而不是与审计数据有关。

2.2. 实现回调方法

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

通常,可移植应用程序的生命周期方法不应调用EntityManagerQuery操作、访问其他实体实例或修改同一持久性上下文中的关系。生命周期回调方法可以修改调用它的实体的非关系状态。 在没有审计框架的情况下,我们必须手动维护数据库模式和域模型。对于我们的简单用例,让我们向实体添加两个新属性,因为我们只能管理“实体的非关系状态”。操作属性将存储执行操作的名称,时间戳属性用于操作的时间戳:

@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,我们可以利用InterceptorEventListeners以及数据库触发器来完成审计。但是 ORM 框架提供了 Envers,一个实现持久类审计和版本控制的模块。

3.1. 开始使用 Envers

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

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

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

@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 可以自动创建它们
  • 使用 o rg.hibernate.tool.EnversSchemaGenerator以编程方式导出完整的数据库模式
  • 设置 Ant 任务以生成适当的 DDL 语句
  • 使用 Maven 插件从我们的映射(例如 Juplo)生成数据库模式以导出 Envers 模式(适用于 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. 配置环境

我们可以像配置任何其他 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 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。该层支持通过扩展 Spring 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注释的列填充有创建或最后修改实体的主体的名称。该信息来自SecurityContextAuthentication实例。如果我们想自定义设置为注释字段的值,我们可以实现AuditorAware*接口:

public class AuditorAwareImpl implements AuditorAware<String> {
 
    @Override
    public String getCurrentAuditor() {
        // your custom logic
    }
}

为了将应用程序配置为使用AuditorAwareImpl来查找当前主体,我们声明了一个AuditorAware类型的 bean,使用 AuditorAwareImpl 实例初始化,并在*@EnableJpaAuditing中将 bean 的名称指定为auditAwareRef*参数的值:

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