在开发应用程序时,我们经常需要存储有关数据如何随时间变化的信息。此信息可用于更轻松地调试应用程序并满足设计要求。在本文中,我们将讨论 JaVers 工具,该工具允许您通过记录数据库实体状态的更改来自动执行此过程。
Javers如何工作?
库的操作基于以 JSON 格式在专用表中存储有关业务数据实体的信息。JaVers允许您将这些信息存储在MongoDB,H2,PostgreSQL,MySQL,MariaDB,Oracle和Microsoft SQL Server等数据库中。
使用MongoDB时,JaVers会创建两个集合来存储审计数据:
- jv_head_id– 用于存储包含commitId 最后一个值的文档的集合
- jv_snapshots– 此集合包含有关由于创建、更新或删除操作而对业务实体所做的更改的详细信息。
另一方面,如果您决定使用其中一个关系数据库,JaVers 将创建以下表:
- jv_global_id– 用于存储每个更改的唯一标识符的表
- jv_commit – 包含有关数据修改的时间和作者信息的表
- jv_commit_property – 一个附加表,允许您存储前一个表中数据的其他信息,例如用户的标识符和用户名。ID 和作为更改作者的用户的名称
- jv_snapshot– 此表存储有关实体的哪些属性因给定操作而更改的信息,以及每个属性的值。
为了提供适当的算法来比较不同版本的对象,JaVers 对几种数据类型进行操作:实体、值对象、容器和基元。
为每个类指示适当的数据类型,可以通过 3 种方式完成:
- 明确地 – 使用寄存器...() 方法或对所选类添加适当的注释
- 隐式 – 依靠 JaVers 根据类层次结构自动检测给定类的类型
- 默认值 – 将所有类视为 ValueObjects
值得注意的是,JaVers 默认映射来自javax.persistence包的注解,这使得在实现我们的数据库实体时无需添加额外的注解。
实现
要开始使用 JaVers 工具,我们需要向项目添加适当的依赖项。根据我们将在其中存储审计数据的数据库,我们有两个选项可供选择:
https://mvnrepository.com/artifact/org.javers/javers-spring-boot-starter-sql- https://mvnrepository.com/artifact/org.javers/javers-spring-boot-starter-mongo
在本文中,我们将使用 PostgreSQL 数据库,因此我们在 pom 文件中包含以下依赖项:
<dependency>
<groupId>org.javers</groupId>
<artifactId>javers-spring-boot-starter-sql</artifactId>
<version>6.6.3</version>
</dependency>
库的工作方式将以应用程序为例呈现,用于存储有关公司正在进行的项目的信息。我们应用程序的数据库实体的结构和实现如下:
Project.java
@Entity
public class Project {
@Id
@GeneratedValue
private UUID id;
@NotNull
private String name;
@NotNull
@Embedded
private ProjectDetails details;
@OneToMany(mappedBy = "project")
private List<Member> members = new ArrayList<>();
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Embeddable
public static class ProjectDetails {
@NotNull
private LocalDateTime startTime;
@NotNull
private LocalDateTime endTime;
}
}
Member.java
@Entity
public class Member {
@Id
@GeneratedValue
private UUID id;
@NotNull
private String name;
@NotNull
private String surname;
@NotNull
private String role;
@ManyToOne
@JoinColumn(name = "project_id")
private Project project;
}
在数据库中,我们应该有以下可用的表列表:
数据审计
要指示哪些业务数据应该接受审计机制,我们可以使用三种方式。
第一种是用@JaversSpringDataAuditable注释标记所选实体的存储库。添加此注释后,调用任何方法来修改此存储库上的数据将导致有关此操作的信息放置在其中一个 JaVers 表中。
在我们的应用程序中,我们只会将注释添加到Project类存储库中,如下所示:
@Repository
@JaversSpringDataAuditable
public interface ProjectRepository extends JpaRepository<Project, UUID> {
}
@Repository
public interface MemberRepository extends JpaRepository<Member, UUID> {
}
服务层方法
@JaversAuditable
public void save(Project project) {
//save project
}
存储库层方法
@Override
@JaversAuditable
S extends Project> S save(S entity);
我们可以保存审计数据的最后一种方法是在Javers类的对象上调用commit方法,我们可以使用依赖注入机制使用它。实际显示此解决方案的代码片段如下所示:
@RequiredArgsConstructor
public class ProjectService {
private final Javers javers;
public void commitProject() {
Project project = new Project();//for simplicity ommited passing parameters
javers.commit("author", project);
}
}
作者提供程序配置
在实践中使用这些方法之前,我们仍然需要指定谁是记录的审计数据的作者。下面显示了用于确定此信息的基本配置示例。
@Configuration
public class JaversAuthorConfiguration {
@Bean
public AuthorProvider provideJaversAuthor() {
return new SimpleAuthorProvider();
}
private static class SimpleAuthorProvider implements AuthorProvider {
@Override
public String provide() {
return "Freddie Mercury";
}
}
}
在实践中审计数据
为了在实践中验证所讨论的机制,调用了插入和修改数据方法,从而生成了INITIAL和UPDATE类型的审计数据。调用每个方法的结果如下所示:
添加新数据
插入新业务数据的方法:
public void save() {
Project.ProjectDetails projectDetails = Project.ProjectDetails.builder()
.startTime(LocalDateTime.now())
.endTime(LocalDateTime.now().plusDays(14))
.build();
Project project = Project.builder()
.name("Project 1")
.details(projectDetails)
.build();
Member member = Member.builder()
.name("Brian")
.surname("May")
.role("guitarist")
.project(project)
.build();
project.getMembers().add(member);
projectRepository.saveAndFlush(project);
}
调用上述方法的结果是在jv_snapshot表中生成的以下条目:
值得注意的是,尽管仅在项目实体存储库上添加了@JaversSpringDataAuditable注释,但成员实体数据也已写入。
默认情况下,JaVers 包括属于审核实体的所有嵌套模型。我们可以通过在要从审计机制中省略的字段上添加@DiffIgnore注释来更改此行为。
在我们的例子中,这个注释的使用如下所示:
@DiffIgnore
@Builder.Default
@OneToMany(mappedBy = "project", cascade = CascadeType.ALL)
private List<Member> members = new ArrayLis<>();
修改现有数据
反过来,修改以前插入的数据的方法是:
public void update(UUID uuid) {
Project project = projectRepository.getById(uuid);
project.setName("Live Aid");
projectRepository.saveAndFlush(project);
}
但是,调用上述方法会导致生成其他条目,如下表所示:
下载审核数据
由于库提供的API,存储的审计数据可以从存储库中检索,称为JaVers查询语言(link:JQL (JaVers Query Language) examples — JaVers Documentation)。可用方法返回的数据可以以以下三种形式之一呈现:
- 影子 – 包含域对象的历史数据,这些数据是从快照(快照)重新创建的
- 更改 – 表示两个对象属性之间的差异
- 快照 – 包含域对象的历史数据,表示为具有值的属性映射
以下是以所讨论的每种形式返回数据的代码片段。此外,在每个代码段下,还显示了调用该方法后返回的数据示例。
影子查询
查询:
public String getShadow() {
JqlQuery query = QueryBuilder.byClass(Project.class).build();
List<Shadow<Object>> shadows = javers.findShadows(query);
return javers.getJsonConverter().toJson(shadows);
}
[
{
"commitMetadata": {
"author": "Freddie Mercury",
"properties": [],
"commitDate": "2022-05-03T20:02:27.554",
"commitDateInstant": "2022-05-03T18:02:27.554689400Z",
"id": 2.00
},
"it": {
"id": "37bfc44c-ab29-44b5-869c-b39b705bdfdf",
"name": "Live Aid",
"details": {
"startTime": "2022-05-03T20:02:12.274037",
"endTime": "2022-05-17T20:02:12.274037"
}
}
},
{
"commitMetadata": {
"author": "Freddie Mercury",
"properties": [],
"commitDate": "2022-05-03T20:02:12.343",
"commitDateInstant": "2022-05-03T18:02:12.343208800Z",
"id": 1.00
},
"it": {
"id": "37bfc44c-ab29-44b5-869c-b39b705bdfdf",
"name": "Project 1",
"details": {
"startTime": "2022-05-03T20:02:12.2740372",
"endTime": "2022-05-17T20:02:12.2740372"
}
}
}
]
如我们所见,返回的响应包含有关同一对象不同版本的信息。例如,返回的数据没有关于给定版本中哪个值已更改的专用信息。
更改查询
查询:
public String getChanges() {
JqlQuery query = QueryBuilder.byClass(Project.class).build();
Changes changes = javers.findChanges(query);
return javers.getJsonConverter().toJson(changes);
}
[
{
"changeType": "ValueChange",
"globalId": {
"entity": "io.devapo.javerssample.entity.Project",
"cdoId": "37bfc44c-ab29-44b5-869c-b39b705bdfdf"
},
"commitMetadata": {
"author": "Freddie Mercury",
"properties": [],
"commitDate": "2022-05-03T20:02:27.554",
"commitDateInstant": "2022-05-03T18:02:27.554689400Z",
"id": 2.00
},
"property": "name",
"propertyChangeType": "PROPERTY_VALUE_CHANGED",
"left": "Project 1",
"right": "Live Aid"
},
{
"changeType": "NewObject",
"globalId": {
"entity": "io.devapo.javerssample.entity.Project",
"cdoId": "37bfc44c-ab29-44b5-869c-b39b705bdfdf"
},
"commitMetadata": {
"author": "Freddie Mercury",
"properties": [],
"commitDate": "2022-05-03T20:02:12.343",
"commitDateInstant": "2022-05-03T18:02:12.343208800Z",
"id": 1.00
}
},
{
"changeType": "InitialValueChange",
"globalId": {
"entity": "io.devapo.javerssample.entity.Project",
"cdoId": "37bfc44c-ab29-44b5-869c-b39b705bdfdf"
},
"commitMetadata": {
"author": "Freddie Mercury",
"properties": [],
"commitDate": "2022-05-03T20:02:12.343",
"commitDateInstant": "2022-05-03T18:02:12.343208800Z",
"id": 1.00
},
"property": "id",
"propertyChangeType": "PROPERTY_VALUE_CHANGED",
"left": null,
"right": "37bfc44c-ab29-44b5-869c-b39b705bdfdf"
},
{
"changeType": "InitialValueChange",
"globalId": {
"entity": "io.devapo.javerssample.entity.Project",
"cdoId": "37bfc44c-ab29-44b5-869c-b39b705bdfdf"
},
"commitMetadata": {
"author": "Freddie Mercury",
"properties": [],
"commitDate": "2022-05-03T20:02:12.343",
"commitDateInstant": "2022-05-03T18:02:12.343208800Z",
"id": 1.00
},
"property": "name",
"propertyChangeType": "PROPERTY_VALUE_CHANGED",
"left": null,
"right": "Project 1"
}
]
在上面的示例中,我们可以看到仅针对Project类中的字段返回更改信息,省略了嵌入的ProjectDetails类型的值。
生成的响应列出了有关对象创建和特定字段值分配的信息。因此,在以Changes 的形式调用数据查询时,值得通过设置适当的筛选器来缩小搜索区域。
快照查询
查询:
public String getSnapshot() {
JqlQuery query = QueryBuilder.byClass(Project.class).build();
List<CdoSnapshot> snapshots = javers.findSnapshots(query);
return javers.getJsonConverter().toJson(snapshots);
}
[
{
"commitMetadata": {
"author": "Freddie Mercury",
"properties": [],
"commitDate": "2022-05-03T20:02:27.554",
"commitDateInstant": "2022-05-03T18:02:27.554689400Z",
"id": 2.00
},
"globalId": {
"entity": "io.devapo.javerssample.entity.Project",
"cdoId": "37bfc44c-ab29-44b5-869c-b39b705bdfdf"
},
"state": {
"name": "Live Aid",
"details": {
"valueObject": "io.devapo.javerssample.entity.Project$ProjectDetails",
"ownerId": {
"entity": "io.devapo.javerssample.entity.Project",
"cdoId": "37bfc44c-ab29-44b5-869c-b39b705bdfdf"
},
"fragment": "details"
},
"id": "37bfc44c-ab29-44b5-869c-b39b705bdfdf"
},
"changedProperties": [
"name"
],
"type": "UPDATE",
"version": 2
},
{
"commitMetadata": {
"author": "Freddie Mercury",
"properties": [],
"commitDate": "2022-05-03T20:02:12.343",
"commitDateInstant": "2022-05-03T18:02:12.343208800Z",
"id": 1.00
},
"globalId": {
"entity": "io.devapo.javerssample.entity.Project",
"cdoId": "37bfc44c-ab29-44b5-869c-b39b705bdfdf"
},
"state": {
"name": "Project 1",
"details": {
"valueObject": "io.devapo.javerssample.entity.Project$ProjectDetails",
"ownerId": {
"entity": "io.devapo.javerssample.entity.Project",
"cdoId": "37bfc44c-ab29-44b5-869c-b39b705bdfdf"
},
"fragment": "details"
},
"id": "37bfc44c-ab29-44b5-869c-b39b705bdfdf"
},
"changedProperties": [
"name",
"details",
"id"
],
"type": "INITIAL",
"version": 1
}
]
在上面的示例中,我们可以看到Snapshot响应包含与Shodow响应类似的数据集,但包含一些其他元素,例如更改值的列表或审计数据类型(初始或更新)。
总结
上面的文章介绍了JaVers工具,它允许您促进和部分自动化数据审计过程。在我们看来,此工具可以让您满足许多项目要求,同时实现它所需的少量工作。我们很高兴与该库分享我们的经验,并鼓励您尝试一下。