Spring Data Envers 支持有条件变动纪录的保存和查询

数据审计是业务系统的一个基本能力,需要系统能够将关键数据的变动纪录都保存下来,并支持变动纪录的查询。

通过spring-data-envers可以很容易的实现数据变动纪录的保存和查询。

有些情况下,我们需要只保存满足特定条件的数据变动纪录,不满足条件的变动纪录不进行保存,例如只保存某个字段有值的变动纪录。

本文介绍支持有条件变动纪录的保存和查询的方法。

具体的代码参照 示例项目 https://github.com/qihaiyan/springcamp/tree/master/spring-data-envers-conditional

一、概述

可以通过 spring-data-envers 很容易的实现变动纪录的保存和查询,只需要增加几个注解就可以。但是要实现有条件的变动纪录的保存和查询就需要进行一些复杂的处理。

二、使用 spring-data-envers

首先引入 spring-data-envers 依赖。

在 build.gradle 中增加一行代码:

implementation 'org.springframework.data:spring-data-envers'

在实体类上增加 Audited 注解:

@Data
@Entity
@Audited
public class MyData {@Id@GeneratedValueprivate Long id;private String author;
}

Repository 扩展 RevisionRepository 方法:

public interface MyDataRepository extends JpaRepository<MyData, Long>, RevisionRepository<MyData, Long, Integer> {
}

通过以上3步操作,就添加好了变动纪录的保存功能,我们可以通过调用变动纪录查询方法确认变动纪录保存成功。

当 Repository 扩展 RevisionRepository 方法后,会有一个默认实现的 findRevisions 方法,我们可以直接调用:

public Revisions<Integer, MyData> findRevisions(Long id) {return myDataRepository.findRevisions(id);
}

最后我们可以执行完整的主体数据的保存,在控制台中打印变动纪录:

@Override
public void run(String... args) {MyData myData = new MyData();myData.setId(1L);myData.setAuthor("test");dbService.saveData(myData);dbService.findRevisions(myData.getId()).forEach(r -> System.out.println("revision: " + r.toString()));myData.setAuthor("newAuthor");dbService.saveData(myData);dbService.findRevisions(myData.getId()).forEach(r -> System.out.println("revision: " + r.toString()));
}

执行完程序后,可以看到两次保存数据的操作都可以查询到对应的变动纪录,并且变动纪录还通过 revisionType 显示了是插入还是更新操作:

revision: Revision 1 of entity MyData(id=1, author=test) - Revision metadata DefaultRevisionMetadata{entity=DefaultRevisionEntity(id = 1, revisionDate = Oct 15, 2023, 11:41:15 AM), revisionType=INSERT}
revision: Revision 2 of entity MyData(id=1, author=newAuthor) - Revision metadata DefaultRevisionMetadata{entity=DefaultRevisionEntity(id = 2, revisionDate = Oct 15, 2023, 11:41:16 AM), revisionType=UPDATE}

三、通过自定义 Event Listener 实现有条件的变动纪录的保存

在进行数据变动时, Envers 通过监听事件来进行对应的处理,总共有以下几个监听事件:

EventType.POST_INSERT
EventType.PRE_UPDATE
EventType.POST_UPDATE
EventType.POST_DELETE
EventType.POST_COLLECTION_RECREATE
EventType.PRE_COLLECTION_REMOVE
EventType.PRE_COLLECTION_UPDATE

每个监听事件都对应着特定的 Listener ,在本文实例中,我们期望当 author 的值被更新为空时,不保存变动纪录,我们可以通过自定义 PRE_UPDATE 和 POST_UPDATE 的Listener来实现。

因为框架提供了默认的Listener,因此自定义 Listener 只需要扩展默认的Listener,并加入我们自己的特有逻辑就可以。

MyEnversPostUpdateEventListenerImpl :

public class MyEnversPreUpdateEventListenerImpl extends EnversPreUpdateEventListenerImpl {public MyEnversPreUpdateEventListenerImpl(EnversService enversService) {super(enversService);}@Overridepublic boolean onPreUpdate(PreUpdateEvent event) {if (event.getEntity() instanceof MyData&& ((MyData) event.getEntity()).getAuthor() == null) {return false;}return super.onPreUpdate(event);}}

MyEnversPostUpdateEventListenerImpl:

public class MyEnversPostUpdateEventListenerImpl extends EnversPostUpdateEventListenerImpl {public MyEnversPostUpdateEventListenerImpl(EnversService enversService) {super(enversService);}@Overridepublic void onPostUpdate(PostUpdateEvent event) {if (event.getEntity() instanceof MyData && ((MyData) event.getEntity()).getAuthor() == null) {return;}super.onPostUpdate(event);}
}

在自定义 Listener 中,我们增加了 对于 author 字段是否为空的判断逻辑。

四、自定义 Event Listener 注册到系统中

自定义 Event Listener 完成后,我们还需要让框架执行我们自定义的 Listener, 而不是用默认的 Listener。

框架通过 EnversIntegrator 类注册的 Listener, 我们要做的是重新实现 EnversIntegrator , 在本实例中重新实现的类为 MyEnversIntegrator :

public class MyEnversIntegrator implements Integrator {@Overridepublic void integrate(Metadata metadata,BootstrapContext bootstrapContext,SessionFactoryImplementor sessionFactory) {final ServiceRegistry serviceRegistry = sessionFactory.getServiceRegistry();final EnversService enversService = serviceRegistry.getService(EnversService.class);final EventListenerRegistry listenerRegistry = serviceRegistry.getService(EventListenerRegistry.class);listenerRegistry.addDuplicationStrategy(EnversListenerDuplicationStrategy.INSTANCE);if (enversService.getEntitiesConfigurations().hasAuditedEntities()) {listenerRegistry.appendListeners(EventType.POST_DELETE,new EnversPostDeleteEventListenerImpl(enversService));listenerRegistry.appendListeners(EventType.POST_INSERT,new EnversPostInsertEventListenerImpl(enversService));listenerRegistry.appendListeners(EventType.PRE_UPDATE,new MyEnversPreUpdateEventListenerImpl(enversService));listenerRegistry.appendListeners(EventType.POST_UPDATE,new MyEnversPostUpdateEventListenerImpl(enversService));listenerRegistry.appendListeners(EventType.POST_COLLECTION_RECREATE,new EnversPostCollectionRecreateEventListenerImpl(enversService));listenerRegistry.appendListeners(EventType.PRE_COLLECTION_REMOVE,new EnversPreCollectionRemoveEventListenerImpl(enversService));listenerRegistry.appendListeners(EventType.PRE_COLLECTION_UPDATE,new EnversPreCollectionUpdateEventListenerImpl(enversService));}}@Overridepublic void disintegrate(SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry) {// nothing to do}
}

通过代码可以发现,我们只是修改了 PRE_UPDATE 和 POST_UPDATE 注册的 Listener , 其它事件的 Listener 仍然用框架默认的。

最后我们需要把我们实现的 MyEnversIntegrator 放到 META-INF/services/org.hibernate.integrator.spi.Integrator 这个配置文件中。

cn.springcamp.springdata.envers.MyEnversIntegrator

五、确认有条件变动纪录的保存是否生效

最后我们修改控制台打印程序,将 author 字段更新为 null 并保存,查看变动纪录里是否有这个更新操作的纪录。

增加保存代码 :

// won't generate audit record when author is null
myData.setAuthor(null);
dbService.saveData(myData);
dbService.findRevisions(myData.getId()).forEach(r -> System.out.println("revision: " + r.toString()));

执行程序并观察控制台打印内容:

revision: Revision 1 of entity MyData(id=1, author=test) - Revision metadata DefaultRevisionMetadata{entity=DefaultRevisionEntity(id = 1, revisionDate = Oct 15, 2023, 11:41:15 AM), revisionType=INSERT}
revision: Revision 2 of entity MyData(id=1, author=newAuthor) - Revision metadata DefaultRevisionMetadata{entity=DefaultRevisionEntity(id = 2, revisionDate = Oct 15, 2023, 11:41:16 AM), revisionType=UPDATE}

通过打印内容可以确认,author 字段更新为 null 的变动纪录没有被纪录,说明我们的处理是生效的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/105661.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

蓝桥杯(迷宫,C++)

输入&#xff1a; 思路&#xff1a; 1、注意输入用字符串。 2、采用广度搜素的方法来求解。 3、因为最后要求字典序最小且D<L<R<U,所以在遍历四个方向的时候&#xff0c; 先向下&#xff0c;再向左、右&#xff0c;最后向上。 #include<iostream> #include…

thinkphp6

unexpected , expecting case (T_CASE) or default (T_DEFAULT) or } 在模板中应用{switch}{/switch}标签,报错,其实是switch的问题&#xff0c;模板解析后&#xff0c;switch:和第一个case:之间不能有有输出的&#xff0c;一个空格也不行&#xff0c;所以第一个要紧跟着 Thi…

基于Jaya优化算法的电力系统最优潮流研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

数据库管理-第109期 19c OCM考后感(20231015)

数据库管理-第109期 19c OCM考后感&#xff08;202301015&#xff09; 距离上一篇又过了两周多&#xff0c;为啥又卡了这么久&#xff0c;主要是后面几个问题&#xff1a;1. 9月1日的19c OCM upgrade考试木有过&#xff0c;因为有一次免费补考机会就又预约了10月8日的考试&…

网络工程师知识点3

41、各个路由协议&#xff0c;在华为设备中的优先级&#xff1f; 直连路由 0 OSPF 10 静态 60 42、OSPF&#xff1a;开放式最短路径优先路由协议&#xff0c;使用SPF算法发现和计算路由 OSPF的优点&#xff1a; 1、收敛速度快&#xff0c;无路由自环&#xff0c;适用于大型网络…

FPGA面试题(6)

一.Verilog中什么情况会产生锁存器&#xff1f; 组合逻辑中&#xff1a;case语句分支不完整&#xff1b;case语句中没有default组合逻辑中&#xff1a;always语句中用if但没有else 二.FPGA和C语言有什么联系&#xff1f;Verilog与C语言的区别&#xff1f; FPGA和C语言的联系…

Docker系列--镜像和容器备份与恢复的方法

原文网址&#xff1a;Docker系列--镜像和容器备份与恢复的方法_IT利刃出鞘的博客-CSDN博客 简介 说明 本文介绍备份和恢复Docker镜像与容器的方法。 命令对比 保存与导出 docker save&#xff1a;保存的是镜像&#xff08;image&#xff09;。&#xff08;保存的是分层的…

解密人工智能:KNN | K-均值 | 降维算法 | 梯度Boosting算法 | AdaBoosting算法

文章目录 一、机器学习算法简介1.1 机器学习算法包含的两个步骤1.2 机器学习算法的分类 二、KNN三、K-均值四、降维算法五、梯度Boosting算法和AdaBoosting算法六、结语 一、机器学习算法简介 机器学习算法是一种基于数据和经验的算法&#xff0c;通过对大量数据的学习和分析&…

通用人工智能技术(深度学习,大模型,Chatgpt,多模态,强化学习,具身智能)

目录 前言 1.通用人工智能 1.1 生物学分析 1.2具身智能 1.2.1当前的人工智能的局限 1.2.2 具身智能实现的基础 1.2.3 强化学习&#xff08;决策大模型&#xff09; 2.结论 往期文章 参考文献 前言 目前的人工智能实质上只是强人工智能&#xff0c;或者说单个领域的通…

软件设计师_面向对象_学习笔记

文章目录 1 面向对象基本概念2 设计模式3 UML4 设计模式4.1 设计模式的基本概念4.2 设计模式的分类4.3 创建型模式 1 面向对象基本概念 2 设计模式 3 UML 4 设计模式 4.1 设计模式的基本概念 模式&#xff1a;通俗的来说就是成功方案的复用。 架构模式从全局看待问题。设计模式…

LiveGBS流媒体平台GB/T28181功能-国标流媒体服务同时兼容内网收流外网收流多网段设备收流

LiveGBS流媒体平台GB/T28181功能-国标流媒体服务同时兼容内网收流外网收流多网段设备收流 1、背景2、设备接入播放2.1、查看通道2.2、直播播放 3、默认收流地址配置4、其它网络设备收流配置5、搭建GB28181视频直播平台 1、背景 服务器部署的时候&#xff0c;可能有多个网卡多个…

JAVA中的垃圾回收

JVM规范说了并不需要必须回收方法区&#xff0c;不具有普遍性&#xff0c;永久代使用的是JVM之外的内存 引用计数:效率要比可达性分析要强&#xff0c;随时发现&#xff0c;随时回收&#xff0c;实现简单&#xff0c;但是可能存在内存泄漏 局部变量表&#xff0c;静态引用变量&…

利达卓越:发挥金融力量,促进团队发展

随着中国经济的快速增长和金融改革的逐步深化&#xff0c;我国金融业取得了令人瞩目的发展。作为经济的重要支柱&#xff0c;我国金融业的规模和实力不断扩大&#xff0c;已经成为全球最大的金融市场之一。利达卓越是一支由管理精英组成的团队&#xff0c;专注于金融行业的投资…

Kafka消费者使用案例

本文代码链接&#xff1a;https://download.csdn.net/download/shangjg03/88422633 1.消费者和消费者群组 在 Kafka 中&#xff0c;消费者通常是消费者群组的一部分&#xff0c;多个消费者群组共同读取同一个主题时&#xff0c;彼此之间互不影响。Kafka 之所以要引入消费者群组…

HTTP 响应头 X-Frame-Options

简介 X-Frame-Options HTTP 响应头用来给浏览器一个指示。该指示的作用为&#xff1a;是否允许页面在 <frame>, </iframe> 或者 <object> 中展现。 网站可以使用此功能&#xff0c;来确保自己网站的内容没有被嵌套到别人的网站中去&#xff0c;也从而避免了…

定制排序小案例

案例&#xff1a;自定义 Book 类&#xff0c;里面包含 name 和 price&#xff0c;按 price 排序(从大到小)。 要求使用两种方式排序 , 有一个 Book[] books 4 本书对象. 使用前面学习过的传递 实现 Comparator 接口匿名内部类&#xff0c;也称为定制排序。 可以按照 price …

【linux】E45: ‘readonly‘ option is set (add ! to override)

vim 编辑文件保存时 E45:设置了“只读”选项&#xff08;添加&#xff01;以覆盖&#xff09; 输入&#xff1a; wq! 提示 "/etc/my.cnf" E212: Cant open file for writing 依然是没有权限&#xff1a; 解决一&#xff1a; 切换用户&#xff1a; su root 解…

黑马店评-04缓存更新策略,保证MySQL数据库中的数据和Redis中缓存的数据一致性

缓存更新策略(数据一致) 更新策略 缓存更新是Redis为了节约内存而设计出来的机制,当我们向Redis插入太多数据时就会导致缓存中的数据过多,所以Redis会对部分数据进行更新即淘汰 低一致性需求(数据长久不发生变化): 使用内存淘汰机制,例如店铺类型信息的查询缓存,因为这部分…

Gitlab CI/CD 入门教程

前言 开发人员常常提到的 CI/CD 是什么&#xff1f; 是用于集成测试的工具&#xff0c;每次提交代码后自动检测、构建和进行单元测试的过程。这一整条流水线式的测试流程我们称之为 pipeline。 入门教程 如何使用 CI/CD? 首先需要确保有可用的 runner&#xff08;如何确保…

Rust初接触

一、什么是Rust Rust 是由 Mozilla 开发的多范式编程语言&#xff0c;专注于性能和安全性。 Rust 以其先进的安全并发能力而闻名&#xff0c; 它的语法类似于 C&#xff0c;但它提供了更快的速度和内存安全性&#xff0c;但不使用垃圾收集器。 Rust 最初是为 Mozilla Firefox …