1. 概述
在ORM的上下文中,数据库审计意味着跟踪和记录与持久实体相关的事件,或者只是实体版本控制。受 SQL 触发器的启发,这些事件是对实体的插入、更新和删除操作。数据库审核的好处类似于源版本控制提供的好处。
在本教程中,我们将演示将审核引入应用程序的三种方法。首先,我们将使用标准 JPA 实现它。接下来,我们将看看两个提供自己的审计功能的JPA扩展,一个由Hibernate提供,另一个由Spring Data提供。
下面是我们将在本例中使用的示例相关实体 Bar和Foo:
2. JPA审计
JPA 没有显式包含审计 API,但我们可以使用实体生命周期事件来实现此功能。
2.1.@PrePersist,@PreUpdate和@PreRemove
在 JPA 实体类中,我们可以指定一个方法作为回调,我们可以在特定实体生命周期事件期间调用该方法。由于我们对在相应的 DML 操作之前执行的回调感兴趣,因此 @PrePersist、@PreUpdate和 @PreRemove回调注释可用于我们的目的:
@Entity
public class Bar {
@PrePersist
public void onPrePersist() { ... }
@PreUpdate
public void onPreUpdate() { ... }
@PreRemove
public void onPreRemove() { ... }
}
Copy
内部回调方法应始终返回void,并且不带任何参数。它们可以具有任何名称和任何访问级别,但不应是静态的。
请注意,JPA 中的@Version注释与我们的主题并不严格相关;它与乐观锁定的关系大于与审计数据的关系。
2.2. 实现回调方法
不过,这种方法存在很大的限制。如 JPA2 规范 (JSR 317) 中所述:
通常,可移植应用程序的生命周期方法不应调用EntityManager或Query操作、访问其他实体实例或修改同一持久性上下文中的关系。生命周期回调方法可以修改调用它的实体的非关系状态。
在没有审计框架的情况下,我们必须手动维护数据库模式和域模型。对于我们的简单用例,让我们向实体添加两个新属性,因为我们只能管理“实体的非关系状态”。操作属性将存储所执行操作的名称,时间戳属性用于操作的时间戳:
@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());
}
}
Copy
如果我们需要将这样的审计添加到多个类中,我们可以@EntityListeners来集中代码:
@EntityListeners(AuditListener.class)
@Entity
public class Bar { ... }
Copy
public class AuditListener {
@PrePersist
@PreUpdate
@PreRemove
private void beforeAnyOperation(Object object) { ... }
}
Copy
3. 冬眠者
使用Hibernate,我们可以利用拦截器和事件侦听器以及数据库触发器来完成审计。但是ORM框架提供了Envers,一个实现持久类的审计和版本控制的模块。
3.1. 开始使用 Envers
要设置 Envers,我们需要将hibernate-enversJAR 添加到我们的类路径中:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-envers</artifactId>
<version>${hibernate.version}</version>
</dependency>
Copy
然后我们添加@Audited注释,要么在 an@Entity(审计整个实体)上,要么在 specific@Columns(如果我们只需要审计特定属性):
@Entity
@Audited
public class Bar { ... }
Copy
请注意,Bar与Foo 之间存在一对多关系。在这种情况下,我们要么还需要审核Foo@Audited在Foo上添加,或者在Bar中对关系的属性进行设置@NotAudited:
@OneToMany(mappedBy = "bar")
@NotAudited
private Set<Foo> fooSet;
Copy
3.2. 创建审计日志表
有几种方法可以创建审核表:
- 将hibernate.hbm2ddl.auto设置为创建,创建-删除或更新,以便Envers可以自动创建它们
- 使用 org.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。修订和foo_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);
Copy
可用属性的完整列表可以在Envers 文档中找到。
3.4. 访问实体历史记录
我们可以以类似于通过休眠条件 API 查询数据的方式查询历史数据。我们可以使用AuditReader界面访问实体的审计历史记录,我们可以通过打开的EntityManager或会话通过AuditReaderFactory 获取:
AuditReader reader = AuditReaderFactory.get(session);
Copy
Envers 提供AuditQueryCreator(由AuditReader.createQuery() 返回)以创建特定于审计的查询。以下行将返回在修订版 #2 修改的所有柱线实例(其中bar_AUDIT_LOG。修订版 = 2):
AuditQuery query = reader.createQuery()
.forEntitiesAtRevision(Bar.class, 2)
Copy
以下是我们如何查询Bar 的修订版。这将导致获取所有状态的所有已审核Bar实例的列表:
AuditQuery query = reader.createQuery()
.forRevisionsOfEntity(Bar.class, true, true);
Copy
如果第二个参数为 false,则结果与REVINFO表联接。否则,仅返回实体实例。最后一个参数指定是否返回已删除的 Bar实例。
然后,我们可以使用AuditEntity工厂类指定约束:
query.addOrder(AuditEntity.revisionNumber().desc());
Copy
4. 春季数据JPA
Spring Data JPA是一个框架,通过在JPA提供程序的顶部添加额外的抽象层来扩展JPA。该层支持通过扩展 Spring JPA 存储库接口来创建 JPA 存储库。
出于我们的目的,我们可以扩展CrudRepository<T,ID扩展Serializable>,通用CRUD操作的接口。一旦我们创建并将存储库注入另一个组件,Spring Data 将自动提供实现,我们就可以添加审计功能了。
4.1. 启用 JPA 审计
首先,我们希望通过注释配置启用审核。为了做到这一点,我们在@Configuration类中添加@EnableJpaAuditing:
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories
@EnableJpaAuditing
public class PersistenceConfig { ... }
Copy
4.2. 添加 Spring 的实体回调侦听器
正如我们已经知道的,JPA 提供了@EntityListeners注释来指定回调侦听器类。Spring Data 提供了自己的 JPA 实体侦听器类,AuditingEntityListener。因此,让我们指定Bar实体的侦听器:
@Entity
@EntityListeners(AuditingEntityListener.class)
public class Bar { ... }
Copy
现在,我们可以在持久化和更新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;
//...
}
Copy
通常,我们将属性移动到基类(注释@MappedSuperClass),所有受审计实体都将扩展该基类。在我们的示例中,为了简单起见,我们将它们直接添加到Bar中。
4.4. 使用 Spring 安全性审核更改的作者
如果我们的应用程序使用 Spring 安全性,我们可以跟踪何时进行更改以及更改者:
@Entity
@EntityListeners(AuditingEntityListener.class)
public class Bar {
//...
@Column(name = "created_by")
@CreatedBy
private String createdBy;
@Column(name = "modified_by")
@LastModifiedBy
private String modifiedBy;
//...
}
Copy
用@CreatedBy和@LastModifiedBy批注的列填充有创建或上次修改实体的主体的名称。该信息来自SecurityContext 的身份验证实例。如果我们想自定义设置为注释字段的值,我们可以实现AuditorAware<T>接口:
public class AuditorAwareImpl implements AuditorAware<String> {
@Override
public String getCurrentAuditor() {
// your custom logic
}
}
Copy
为了将应用程序配置为使用AuditorAwareImpl查找当前主体,我们声明了一个AuditorAware类型的 bean,使用AuditorAwareImpl 的实例进行初始化,并将 Bean 的名称指定为auditorAwareRef参数的值@EnableJpaAuditing:
@EnableJpaAuditing(auditorAwareRef="auditorProvider")
public class PersistenceConfig {
//...
@Bean
AuditorAware<String> auditorProvider() {
return new AuditorAwareImpl();
}
//...
}
Copy
5. 结论
在本文中,我们研究了实现审核功能的三种方法:
- 纯 JPA 方法是最基本的,包括使用生命周期回调。但是,我们只允许修改实体的非关系状态。这使得@PreRemove回调对我们的目的毫无用处,因为我们在方法中所做的任何设置都将与实体一起删除。
- Envers是Hibernate提供的成熟审计模块。它是高度可配置的,并且缺乏纯JPA实现的缺陷。因此,它允许我们审核删除操作,因为它记录到实体表以外的表中。
- Spring Data JPA 方法抽象了使用 JPA 回调的工作,并为审计属性提供了方便的注释。它也准备好与Spring Security集成。缺点是它继承了 JPA 方法的相同缺陷,因此无法审核删除操作。
本文的示例在GitHub 存储库中提供。