
本文深入探讨了在使用javers进行java springboot应用审计时,如何解决在一对多关系中,`listchange`对象仅提供子实体引用id而非实际对象值的问题。通过详细阐述`javers.findchanges`的局限性,并引入`javers.findshadows`方法,结合`withchangedpropertyin`和`tocommitid`等查询构建器,教程将指导开发者有效地检索历史版本中子实体的完整对象状态,从而实现精确的变更追踪和审计。
在现代企业级应用中,对数据变更进行审计是至关重要的一环。Javers作为一款强大的Java对象审计库,能够帮助开发者追踪实体对象的历史版本和变更。然而,在处理复杂的一对多关系时,开发者可能会遇到一个常见的问题:当子实体(集合中的元素)发生变更时,Javers的ListChange对象可能仅提供子实体的全局ID(Global Id)引用,而非其完整的对象值,这给理解具体变更内容带来了挑战。
审计场景与问题描述
考虑一个典型的父子实体关系,例如一个ParentEntity包含一个ChildEntity列表:
import org.hibernate.annotations.GenericGenerator;
import javax.persistence.*;
import java.util.List;
import java.util.UUID;
import lombok.Getter;
import lombok.Setter;
import lombok.NoArgsConstructor;
@Entity
@Getter
@Setter
@NoArgsConstructor
class ParentEntity {
@Id
@GenericGenerator(name = "UUIDGenerator", strategy = "uuid2")
@GeneratedValue(generator = "UUIDGenerator")
@Column(name = "id", updatable = false, nullable = false)
private UUID id;
@Column(name = "customer_id")
private String customerId;
@OneToMany(mappedBy = "parentEntity", cascade = CascadeType.ALL, orphanRemoval = true)
private List childEntity;
} import org.hibernate.annotations.GenericGenerator;
import javax.persistence.*;
import java.util.UUID;
import lombok.Getter;
import lombok.Setter;
@Entity
@Getter
@Setter
class ChildEntity {
@Id
@GenericGenerator(name = "UUIDGenerator", strategy = "uuid2")
@GeneratedValue(generator = "UUIDGenerator")
@Column(name = "id", updatable = false, nullable = false)
protected UUID id;
@ManyToOne(fetch = FetchType.LAZY)
private ParentEntity parentEntity;
// 其他属性,例如 String name; int value; 等
private String name;
private int value;
}当ParentEntity中的childEntity列表发生变化,例如某个ChildEntity的属性被修改时,如果使用javers.findChanges方法查询变更,其结果可能包含一个ListChange对象。然而,这个ListChange对象在表示变更的left和right侧时,往往只包含ChildEntity的全局ID,而不是其完整的属性值。这意味着,仅凭ListChange,我们无法直接得知ChildEntity的具体哪个属性从什么值变为什么值。
例如,以下查询通常用于获取指定实例的变更:
import org.javers.core.Javers; import org.javers.repository.jql.QueryBuilder; import org.javers.core.diff.Change; import java.util.List; import java.util.UUID; // 假设javers是Javers实例 // 假设parentId是ParentEntity的UUID // 假设ParentEntity.class是实体类 Listchanges = javers.findChanges( QueryBuilder.byInstanceId(parentId, ParentEntity.class) .withSnapshotTypeUpdate() .build() ); // 遍历changes时,如果遇到ListChange,其中的元素可能只有ID
这种情况下,我们得到的ListChange可能类似于:ListChange for childEntity, changed object: GlobalId(ChildEntity#id=UUID_A) -> GlobalId(ChildEntity#id=UUID_A),这并不能直接展示ChildEntity内部属性的变化。
解决方案:利用Javers的Shadows机制
为了获取子实体的完整对象值,我们需要利用Javers的“影子”(Shadows)机制。Shadows是Javers存储的历史对象快照,它们包含了实体在特定提交(Commit)时的完整状态。通过查询这些Shadows,我们可以重建出对象在不同时间点的完整视图。
Javers提供了javers.findShadows方法来检索这些历史快照。结合QueryBuilder,我们可以精确地定位到感兴趣的实体及其特定属性在某个提交时的状态。
核心方法与查询构建
要获取ChildEntity的实际值,我们可以采用以下查询模式:
import org.javers.core.Javers; import org.javers.repository.jql.QueryBuilder; import org.javers.core.commit.CommitId; import org.javers.core.json.JsonConverter; import org.javers.shadow.Shadow; import java.util.List; import java.util.UUID; // 假设javers是Javers实例 // 假设parentId是ParentEntity的UUID // 假设commitIdStr是导致ChildEntity变更的CommitId字符串,例如从findChanges中获取 // 假设ParentEntity.class是实体类 // 1. 根据CommitId和ParentEntity ID获取ParentEntity的Shadow List> parentShadows = javers.findShadows( QueryBuilder.byInstanceId(parentId, ParentEntity.class) .withChangedPropertyIn("childEntity") // 可选,但有助于优化,聚焦于childEntity属性的变更 .toCommitId(CommitId.parse(commitIdStr)) // 获取特定CommitId时的状态 .build() ); // 2. 从获取到的ParentEntity Shadow中提取ChildEntity的完整对象 if (!parentShadows.isEmpty()) { // 通常只会有一个匹配的ParentEntity Shadow Shadow
查询参数详解:
- QueryBuilder.byInstanceId(Id, ParentEntity.class): 这是查询的基础,指定我们要查询的是哪个ParentEntity实例的历史快照。Id是ParentEntity的唯一标识符(通常是主键)。
- withChangedPropertyIn("childEntity"): 这是一个重要的优化参数。它告诉Javers我们只对那些childEntity属性发生过变更的快照感兴趣。虽然不是强制性的,但在大型数据集中,它可以显著提高查询效率。
- toCommitId(CommitId.parse(commitIdStr)): 这是获取特定时间点对象状态的关键。commitIdStr是从ListChange或其他Change对象中获取到的提交ID。通过指定这个ID,Javers会返回在该提交发生时ParentEntity及其关联ChildEntity的完整状态。
工作流程建议:
- 识别变更点: 首先,使用javers.findChanges(QueryBuilder.byInstanceId(...).build())来获取ParentEntity的所有变更。
- 定位ListChange: 在遍历Change列表时,识别出类型为ListChange的变更,并从中提取出相关的commitId以及涉及到的ChildEntity的ID(如果ListChange提供了)。
-
获取前后快照:
- 使用上述javers.findShadows方法,传入识别出的ParentEntity的ID和commitId,获取发生变更时的ParentEntity快照。
- 如果需要对比变更前后的状态,还需要找到commitId之前的最近一个提交ID,并再次调用findShadows获取变更前的ParentEntity快照。
- 比较并分析: 拿到两个不同commitId下的ParentEntity快照后,你可以比较它们各自childEntity列表中的ChildEntity对象,从而精确地分析出哪个ChildEntity的哪个属性发生了何种变化。
注意事项与最佳实践
- 性能考量: findShadows会返回对象的完整快照,这可能比仅获取变更引用消耗更多的内存和处理时间。在设计审计查询时,应根据实际需求权衡。
- CommitId管理: 准确地获取和管理CommitId是进行精确历史查询的关键。Javers的Change对象通常会包含其所属的CommitId。
- 懒加载与Javers: 确保Javers配置正确处理JPA的懒加载(Lazy Loading)。通常,Javers在处理实体时会尝试加载所有属性,但这取决于你的Javers配置和JPA实体定义。如果ChildEntity的属性是懒加载的,在获取Shadow后访问这些属性时可能需要额外的处理。
-
Shadows的类型安全: findShadows返回List
>,你需要将其强制转换为具体的实体类型(如ParentEntity)才能访问其属性。
总结
当Javers的ListChange对象在处理一对多关系时仅提供子实体的引用ID,而无法直接获取其完整值时,javers.findShadows方法是解决此问题的关键。通过结合QueryBuilder的byInstanceId、withChangedPropertyIn和toCommitId等方法,开发者可以精确地查询到特定提交时实体对象的完整快照。这种方法使得在复杂的审计场景中,能够深入分析并理解集合中子实体的具体变更内容,从而实现更精细、更准确的历史数据追踪。









