# jdbc-plus **Repository Path**: misons/jdbc-plus-master ## Basic Information - **Project Name**: jdbc-plus - **Description**: 强大又简单易用的跨数据库ORM框架,用java语言开发,支持oracle, mysql, sqlserver等数据库无感知切换,不再手写sql脚本,让程序员在繁杂易错的sql脚本的书写中解脱出来,全部用lambda表达式来实现查询,编译期就能将95%以上的脚本问题发现,同时支持多表连接查询。提供跨数据库分页功能,提供拦截器支持,并做了充分的单元测试和spring boot集成 - **Primary Language**: Java - **License**: GPL-2.0 - **Default Branch**: pre-realease - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 6 - **Created**: 2025-06-30 - **Last Updated**: 2025-06-30 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # jdbc-plus #### 介绍 跨数据库ORM框架 jdbc-plus不仅仅是简单的类似MyBatis或者是Hibernate,或者是俩者的综合,jdbc-plus远大理想是实现跨数据库的CRUD统一, 并且支持多表连接的lambda表达式查询 #### 软件架构 最简单最强大的ORM框架jdbc-plus 1. 实体的CRUD直接用泛型实现,完全不需要定义dao层和service层,用公共方法即可调用 2. 可由实体自动建表,建索引,建外键,生成字段约束 3. 可由表反向生成实体 4. 实体注解简单,使用简单 5. 跨数据库, mysql, oracle,sql server数据库无感知切换 6. 程序支持数据源的跨数据库备份和还原 7. 主键可由策略自动生成 8. 多表连接查询全部用户函数式编程实现 9. 多表查询的子查询也全部用函数式编程实现 10. 提供自动检测数据库结构是否一致和自动更新数据库结构功能 11. 不再直接手写SQL,所以编译期能将95%以上的脚本问题不一致问题发现 12. 项目后期维护更简单,修改字段名只需修改注解 13. 拦截器支持,所有表的CRUD可统一处理 14. 提供实体新增时自动填充功能和默认值功能 15. 跨数据库分页功能支持 16. 提供批量插入和批量更新功能 17. 充分的单元测试和spring boot集成 18. 多次运用在实际项目中,大大提高了项目开发和维护成本 19. jdbc-plus写DDL语句就像是直接写Java代码,非常方便 20. 不需要拼接各种字符串来拼接SQL语句(拼接SQL多痛苦在用JDBC的时候经历过,少一个空格都会折磨死) 21. 不用去写各种ResultSet、PrepareStatement 有关的代码 22. 同样支持字符串拼接SQL(不提倡拼接SQL,可能引起SQL注入攻击) 23. 领域模型开发,建议先建实体添加注解,自动生成数据库表结构 24. 高性能,实体反射得到的信息被缓存到内存中,执行CRUD性能和纯JDBC脚本实现方法基本相同 25. 扩展性强,已经实现mysql, oracle,sql server方言,后期可考虑添加其它数据库方言支持 26. 安全性, 前端传入SQL攻击脚本无用,只要不在后台直接拼接SQL脚本即可防止SQL注入攻击 27. 易用性,上手简单快捷,不再手写CRUD操作的sql脚本代码和,减少大部分service层和dao层类定义 28. 可维护性,项目后期维护成本低,项目运行期BUG比用mybatis或者纯jdbc脚本少得多 29. 运用常用设计模式,提高框架扩展性和性能。如数据库方言用策略模式,对象生产用工厂模式,单例模式,享元模式,模板方法模式,代理模式,拦截器模式,建造者模式,链式方法模式 ## 组织结构 ``` lua jdbc-plus-master ├── jdbc-plus-common -- 通用代码模块 ├── jdbc-plus-core -- 核心代码实现模块 ├── jdbc-plus-generator -- 代码生成器和反向生成数据表模块 ├── jdbc-plus-boot-starter -- 与spring-boot集成模块 ├── jdbc-plus-boot-starter-test -- 与spring-boot集成后实例测试模块 ├── jdbc-plus-test -- 单元测试模块 ``` ## 适合用户 * 你不想把精力浪费在简单据库增删改查上?jdbc-plus 内置数据库的CRUD功能 * 你是对代码可维护性有高要求的架构师?jdbc-plus的设计目的就是尽可能提高数据库访问代码可维护性 * 平台级产品需要跨库,支持各种客户数据库的?jdbc-plus 支持各种库,程序员编写一次,能运行到各种数据库 ## 阅读源码例子 可以从模块`jdbc-plus-test`和`jdbc-plus-boot-starter-test`中找得到所有例子,运行单元测试例子。 所有例子都是基于mysql, sqlServer, oracle数据库,可以反复运行 ## 代码示例 ### 例子1,内置方法,无需写SQL完成用户表常用CRUD操作 ```java @ApiOperation(value = "业务创建", notes = "业务创建", produces = "application/json") @PostMapping(value = "/add") public R create(@RequestBody @Valid SysUser user){ DaoUtil.BASE.add(user); return R.success("成功"); } @ApiOperation(value = "新增多条数据", notes = "新增多条数据", produces = "application/json") @PostMapping(value = "/addList") public R addList(@RequestBody @Valid List list){ DaoUtil.BASE.addList(list); return R.success("新增多条成功"); } @ApiOperation(value = "根据id修改", notes = "根据id修改", produces = "application/json") @PostMapping(value = "/editById") public R editById(@RequestBody @Valid SysUser user){ try{ DaoUtil.BASE.update(user); return R.success("成功"); }catch(Exception e){ log.error("根据id修改业务模块异常",e); return R.fail(ResultCode.FAILURE); } } @ApiOperation(value = "通过id删除", notes = "根据id删除一条或者多条数据") @DeleteMapping(value = "/deleteById") public R deleteById(@RequestParam(name = "id", required = true) String id){ try{ DaoUtil.BASE.delete(id,SysUser.class); return R.success("删除成功"); }catch(Exception e){ log.error(e.toString(),e); return R.fail("删除失败,原因:"+e.toString()); } } @ApiOperation(value = "根据ids批量删除", notes = "根据ids批量删除") @DeleteMapping(value = "/deleteBatch") public R deleteBatch(@RequestParam(name = "ids", required = true) String ids){ try{ String[]idSplit=ids.split(","); List pkList=Arrays.asList(idSplit); // 删除多条数据 DaoUtil.BASE.delete(pkList,SysUser.class); return R.success("批量删除成功"); }catch(Exception e){ log.error(e.toString(),e); return R.fail("删除失败,原因:"+e.toString()); } } @ApiOperation(value = "分页列表查询", notes = "分页列表查询") @PostMapping(value = "/page") public R>queryPage(){ EntityWrap wrap=new EntityWrap<>(SysUser.class); wrap.lambda().like(SysUser::getUsername,"小明"); IPage page=new Page<>(1,10); IPage pageList=DaoUtil.BASE.page(page,wrap); return R.data(pageList); } @ApiOperation(value = "查询列表", notes = "根据条件查询列表数据") @PostMapping(value = "/queryList") public R>queryList(){ EntityWrap wrap=new EntityWrap<>(SysUser.class); wrap.lambda().like(SysUser::getUsername,"小明"); List list=DaoUtil.BASE.find(wrap); return R.data(list); } ``` 内置接口的完整方法如下 ``` public interface IBaseDao { /** * 新增实体 * * @param entity * @param * @return */ String add(M entity); /** * 新增或者修改 *

实体主键为空时新增,实体主键非空先查找是否存在,存在则更新非空值,不存在则新增

* * @param entity * @param * @return */ boolean addOrUpdate(M entity); /** * 新增或者修改实体列表 * * @param entityList * @param * @return */ boolean addOrUpdateList(Collection entityList); /** * 复制实体列表 * * @param entityList * @param * @return */ boolean copyList(List entityList); /** * 新增实体列表 * * @param entityList * @param * @return */ boolean addList(List entityList); /** * 根据id和更新条件更新 * * @param * @return */ boolean updateById(String id, UpdateWrap updateWrap); /** * 只更新实体中非空字段, 实体主键属性值不可为空 * * @param entity * @param * @return */ boolean updateById(M entity); /** * 按条件更新实体中非空字段 * * @param entity * @param wrap * @param * @return */ boolean update(M entity, EntityWrap wrap); /** * 按条件更新实体中指定字段 * * @param updateWrap 指定的字段 * @param entityWrap 条件 * @param * @return */ boolean update(UpdateWrap updateWrap, EntityWrap entityWrap); /** * 更新字段 * * @param columnMap * @param wrap * @param * @return */ boolean updateByMap(Map columnMap, EntityWrap wrap); /** * 更新实体列表 * * @param entityList * @param * @return */ boolean updateList(Collection entityList); /** * 更新实体中所有字段, 实体主键属性值不可为空 * * @param entity * @param * @return */ boolean updateAll(M entity); /** * 根据id删除实体 * * @param id * @param entityClass * @param * @return */ boolean deleteById(String id, Class entityClass); /** * 根据id列表删除实体 * * @param ids * @param entityClass * @param * @return */ boolean deleteByIds(Collection ids, Class entityClass); /** * 根据条件删除实体 * * @param wrap * @param * @return */ boolean delete(EntityWrap wrap); /** * 删除全部实体 * * @param entityClass * @param * @return */ boolean deleteAll(Class entityClass); /** * 根据id物理删除实体 * * @param id * @param entityClass * @param * @return */ boolean realDelById(String id, Class entityClass); /** * 根据id列表物理删除实体 * * @param ids * @param entityClass * @param * @return */ boolean realDelByIds(Collection ids, Class entityClass); /** * 根据条件物理删除实体 * * @param wrap * @param * @return */ boolean realDel(EntityWrap wrap); /** * 物理删除全部实体 * * @param entityClass * @param * @return */ boolean realDelAll(Class entityClass); /** * 获取一个实体 * * @param id * @param entityClass * @param * @return */ M getById(String id, Class entityClass); /** * 根据条件获取一个实体 * * @param wrap * @param * @return */ M get(EntityWrap wrap); /** * 查询id对应的实体是否存在 * * @param id * @param entityClass * @param * @return */ boolean exists(String id, Class entityClass); /** * 根据条件查询实体是否存在 * * @param wrap * @param * @return */ boolean exists(EntityWrap wrap); /** * 查询实体总数 * * @param entityClass * @param * @return */ int countAll(Class entityClass); /** * 根据条件查询实体数量 * * @param wrap * @param * @return */ int count(EntityWrap wrap); /** * 查询全部实体列表 * * @param entityClass * @param * @return */ List listAll(Class entityClass); /** * 根据条件查询实体列表 * * @param wrap * @param * @return */ List list(EntityWrap wrap); /** * 根据id列表查询实体列表 * * @param idList * @param entityClass * @param * @return */ List listByIds(Collection idList, Class entityClass); /** * 查询分页参数和条件查询实体分页 * * @param page * @param wrap * @param * @return */ IPage page(IPage page, EntityWrap wrap); } ``` 集成spring-boot,通过 DaoUtil.BASE 即可调用以上所有方法,因为是泛型实现,实体类只需继承于 Entity 即可 ### 例子2 Select接口单元测试 ```java @Test public void selectDistinct(){ QueryLambdaWrap lambdaWrap=new QueryLambdaWrap(); lambdaWrap .selectDistinct(true) .select(SysUser::getEmail) .from(SysUser.class); List emailList=lambdaWrap.findList(String.class,DsUtil.getMysql()); System.out.println(JsonUtil.toPrettyJson(emailList)); } ``` ### 例子3 From接口单元测试 ```java @Test public void from(){ QueryLambdaWrap lambdaWrap=new QueryLambdaWrap(); lambdaWrap .selectAll(SysUser.class) .from(SysUser.class); List userList=lambdaWrap.findList(SysUser.class,DsUtil.getMysql()); String sql=lambdaWrap.getExecSqlFormat(); log.info("exec sql format: "+sql); log.info(JsonUtil.toPrettyJson(userList)); } ``` ### 例子4 Where接口单元测试 ```java /** * 大于等于子查询结果 */ @Test public void geFunc(){ QueryLambdaWrap lambdaWrap=new QueryLambdaWrap(); lambdaWrap .selectAll(SysUser.class) .from(SysUser.class) .ge(SysUser::getId,e->e .select(SysUserRole::getUserId) .from(SysUserRole.class) .eq(SysUserRole::getRoleId,"1") ); List userList=lambdaWrap.findList(SysUser.class,DsUtil.getMysql()); log.info(JsonUtil.toPrettyJson(userList)); } ``` ### 例子5 GroupBy接口单元测试 ```java /** * 分组:GROUP BY 字段, ... *

例: groupBy("id", "name")

*/ @Test public void groupBys(){ QueryLambdaWrap lambdaWrap=new QueryLambdaWrap(); lambdaWrap .selectAs(SysUser::getStatus,DictVO::getText) .selectCountAs(SysUser::getId,DictVO::getValue) .from(SysUser.class) .groupBy(SysUser::getStatus,SysUser::getSex); List userList=lambdaWrap.findList(DictVO.class,DsUtil.getMysql()); log.info(JsonUtil.toPrettyJson(userList)); } ``` ### 例子6 Having接口单元测试 ```java /** * 自定义having条件 *

* 例: having(i -> i.le("id", 10).gt("price", 1)) * 等价于 having id <= 10 and price > 1 *

*/ @Test public void having(){ QueryLambdaWrap lambdaWrap=new QueryLambdaWrap(); lambdaWrap .selectAs(SysUser::getStatus,DictVO::getText) .selectCountAs(SysUser::getId,DictVO::getValue) .from(SysUser.class) .groupBy(SysUser::getStatus,SysUser::getSex) .having(e->e .ge(SysUser::getStatus,0) .ge(SysUser::getSex,0) ); List userList=lambdaWrap.findList(DictVO.class,DsUtil.getMysql()); log.info(JsonUtil.toPrettyJson(userList)); } ``` ### 例子7 OrderBy接口单元测试 ```java /** * 排序 *

* 例 orderByCount(true, "id") * 等价于 order by count(id) asc *

* * @return */ @Test public void orderByCount() { QueryLambdaWrap lambdaWrap = new QueryLambdaWrap(); lambdaWrap .selectAs(SysUser::getStatus, DictVO::getText) .selectCountAs(SysUser::getId, DictVO::getValue) .from(SysUser.class) .groupBy(SysUser::getStatus, SysUser::getSex) .orderByCount(true, SysUser::getId); List userList = lambdaWrap.findList(DictVO.class, DsUtil.getMysql()); log.info(JsonUtil.toPrettyJson(userList)); } ``` ### 例子8 Union接口单元测试 ```java @Test public void union(){ String username="admin"; String menuId="1"; QueryLambdaWrap lambdaWrap=new QueryLambdaWrap(); lambdaWrap .select(SysRoleMenu::getDataRuleIds) .from(SysRoleMenu.class) .innerJoin(SysMenu.class,SysMenu::getId,SysRoleMenu::getPermissionId) .innerJoin(SysRole.class,SysRole::getId,SysRoleMenu::getRoleId) .innerJoin(SysUserRole.class,SysUserRole::getRoleId,SysRole::getId) .innerJoin(SysUser.class,SysUser::getId,SysUserRole::getUserId) .eq(SysUser::getUsername,username) .eq(SysMenu::getId,menuId) .union(lb->lb .select(SysDepartRoleMenu::getDataRuleIds) .from(SysDepartRoleMenu.class) .innerJoin(SysMenu.class,SysMenu::getId,SysDepartRoleMenu::getPermissionId) .innerJoin(SysDepartRole.class,SysDepartRole::getId,SysDepartRoleMenu::getRoleId) .innerJoin(SysDepartRoleUser.class,SysDepartRoleUser::getDroleId,SysDepartRole::getId) .innerJoin(SysUser.class,SysUser::getId,SysDepartRoleUser::getUserId) .eq(SysUser::getUsername,username) .eq(SysMenu::getId,menuId) ); String sql=lambdaWrap.getExecSqlFormat(); System.out.println(sql); List list=lambdaWrap.findList(String.class,DsUtil.getMysql()); log.info(JsonUtil.toPrettyJson(list)); } ``` ### 例子9 复杂多表连接查询 ```java public List queryDataRuleIds(String username,String permissionId){ // // QueryLambdaBootstrap bootstrap=new QueryLambdaBootstrap(); bootstrap .select(SysRoleMenu::getDataRuleIds) .from(SysRoleMenu.class) .innerJoin(SysMenu.class,SysMenu::getId,SysRoleMenu::getPermissionId) .innerJoin(SysRole.class,SysRole::getId,SysRoleMenu::getRoleId) .innerJoin(SysUserRole.class,SysUserRole::getRoleId,SysRole::getId) .innerJoin(SysUser.class,SysUser::getId,SysUserRole::getUserId) .eq(SysUser::getUsername,username) .eq(SysMenu::getId,permissionId) .union(lb->lb .select(SysDepartRoleMenu::getDataRuleIds) .from(SysDepartRoleMenu.class) .innerJoin(SysMenu.class,SysMenu::getId,SysDepartRoleMenu::getPermissionId) .innerJoin(SysDepartRole.class,SysDepartRole::getId,SysDepartRoleMenu::getRoleId) .innerJoin(SysDepartRoleUser.class,SysDepartRoleUser::getDroleId,SysDepartRole::getId) .innerJoin(SysUser.class,SysUser::getId,SysDepartRoleUser::getUserId) .eq(SysUser::getUsername,username) .eq(SysMenu::getId,permissionId) ); return bootstrap.findList(String.class); } ``` ### 例子10 多个查询合并在同一个DAO方法中 这样好处是方便数据库DBA与程序员沟通 ```java /** * 根据用户查询用户权限 * * @param username */ @Override public List queryByUser(String username){ QueryLambdaBootstrap bootstrap=new QueryLambdaBootstrap(); bootstrap .selectAll(SysMenu.class) .from(SysMenu.class) .eq(SysMenu::getDelFlag,0) .and(e->e .exists(lb->lb .select(SysRoleMenu::getId) .from(SysRoleMenu.class) .innerJoin(SysRole.class,SysRole::getId,SysRoleMenu::getRoleId) .innerJoin(SysUserRole.class,SysUserRole::getRoleId,SysRole::getId) .innerJoin(SysUser.class,SysUser::getId,SysUserRole::getUserId) .on(SysMenu::getId,SysRoleMenu::getPermissionId) .eq(SysUser::getUsername,username) ) .or(l->l .likeRight(SysMenu::getUrl,":code") .likeLeft(SysMenu::getUrl,"/online") .eq(SysMenu::isHidden,true) ) .or(l->l.eq(SysMenu::getUrl,"/online")) ); QueryLambdaBootstrap bootstrap1=new QueryLambdaBootstrap(); bootstrap1 .selectAll(SysMenu.class) .from(SysMenu.class) .eq(SysMenu::getDelFlag,0) .and(e->e .exists(lb->lb .select(SysDepartRoleMenu::getId) .from(SysDepartRoleMenu.class) .innerJoin(SysDepartRole.class,SysDepartRole::getId,SysDepartRoleMenu::getRoleId) .innerJoin(SysDepartRoleUser.class,SysDepartRoleUser::getDroleId,SysDepartRole::getId) .innerJoin(SysUser.class,SysUser::getId,SysDepartRoleUser::getUserId) .on(SysMenu::getId,SysDepartRoleMenu::getPermissionId) .eq(SysUser::getUsername,username) ) ); //lambdaWrap.union(i -> lambdaWrap2); log.info("sql exec 1: "+bootstrap.getExecSql()); List list=bootstrap.findList(SysMenu.class); List list2=bootstrap1.findList(SysMenu.class); List result=new ArrayList<>(); result.addAll(list); result.addAll(list2); result=result.stream().sorted(Comparator.comparing(l->l.getSortNo(),Comparator.nullsLast(Double::compareTo))).collect(Collectors.toList()); return result; // SELECT * FROM ( // SELECT p.* // FROM sys_permission p // WHERE (exists( // select a.id from sys_role_permission a // join sys_role b on a.role_id = b.id // join sys_user_role c on c.role_id = b.id // join sys_user d on d.id = c.user_id // where p.id = a.permission_id AND d.username = #{username,jdbcType=VARCHAR} // ) // or (p.url like '%:code' and p.url like '/online%' and p.hidden = 1) // or p.url = '/online') // and p.del_flag = 0 // // UNION // SELECT p.* // FROM sys_permission p // WHERE exists( // select a.id from sys_depart_role_permission a // join sys_depart_role b on a.role_id = b.id // join sys_depart_role_user c on c.drole_id = b.id // join sys_user d on d.id = c.user_id // where p.id = a.permission_id AND d.username = #{username,jdbcType=VARCHAR} // ) // and p.del_flag = 0 // // ) h order by h.sort_no ASC } ``` ### 例子11 根据表结构生成实体 可以使用内置的代码生成框架生成代码何文档,也可以自定义的,用户可自行扩展SourceBuilder类 ```java public static void main(String[]args){ // 生成指定表名列表对应的实体 ConfigTableVo vo=new ConfigTableVo(); vo.setAuthor("william"); vo.setDataSource(getDs()); //实体所在包名前缀 vo.setPackageName("com.william.jdbcplus"); //模块名 vo.setModuleName("generator"); List tables=new ArrayList<>(); // 必须与数据库大小写一致 tables.add("excel_report"); tables.add("excel_report_data_source"); tables.add("excel_report_db"); tables.add("excel_report_db_field"); tables.add("excel_report_db_param"); vo.setTableNames(tables); vo.setTablePrefix(""); byte[]data=entityCode(vo); outputFile(data,"D:\\temp","report.zip"); //生成数据库所有表对应的实体列表 ConfigVO configVO=new ConfigVO(); configVO.setAuthor("william"); configVO.setDataSource(getDs()); //实体所在包名前缀 configVO.setPackageName("com.william.jdbcplus"); //模块名 configVO.setModuleName("generator"); configVO.setTablePrefix(""); byte[]dataList=entityCodes(configVO); outputFile(dataList,"D:\\temp","reportAll.zip"); } ``` ### 例子13 根据实体更新表结构 ```java public static void main(String[]args){ //指定数据源 DataSource ds=getDs(); // 根据模块更新数据库 DDLUtil.updateDatabase("com.william.jdbcplus.generator",ds); // 更新单表,表不存在则创建表,表存在则更新表结构 DDLUtil.updateTable(ExcelReport.class,ds); DDLUtil.updateTable(ExcelReportDataSource.class,ds); DDLUtil.updateTable(ExcelReportDb.class,ds); DDLUtil.updateTable(ExcelReportDbField.class,ds); DDLUtil.updateTable(ExcelReportDbParam.class,ds); } ``` ### 例子14 DDL测试 ```java public void checkDatabase(){ DataSource ds=DBUtil.getMysql(); List list=DDLUtil.checkDatabase("com.william.jdbcplus.test",ds); list.stream().forEach(e->System.out.println(e)); } ``` ### 例子15 备份数据库测试 ```java /** * 指定路径备份数据到json文件中 */ public void backupToJson(){ String path="D:\\DEV\\SOURCE\\aliyun\\jdbc-plus-master.git\\jdbc-plus-test\\src\\main\\resources\\backup\\"; DataSource ds=DBUtil.getMysql(); CrudUtil.backupToJson("com.william.jdbcplus.test",ds,path); } /** * 备份数据到指定数据库 */ public void backupToDB(){ DataSource dstDs=DBUtil.getOracle(); DataSource srcDs=DBUtil.getMysql(); DDLUtil.updateDatabase("com.william.jdbcplus.test",dstDs); CrudUtil.backupToDB("com.william.jdbcplus.test",srcDs,dstDs); } ``` ### 例子16 还原数据库测试 ```java public void recover(){ String path="D:\\DEV\\SOURCE\\aliyun\\jdbc-plus-master.git\\jdbc-plus-test\\src\\main\\resources\\backup\\"; DataSource ds=DBUtil.getMysql(); DDLUtil.updateDatabase("com.william.jdbcplus.test",ds); CrudUtil.recover(path,ds); } ``` ### 例子17 事务测试 ```java public void insertBatch(){ DaoUtil.TX.execute(new TransactionCallback(){ @Override public Object doInTransaction(TransactionStatus transactionStatus){ // DML执行 try{ List list=new ArrayList<>(); for(int i=0;i< 100;i++){ SysDemo entity=new SysDemo(); entity.setCode("456"+i); entity.setName("小明"+i); entity.setTimeUnit(TimeUnitEnum.SECOND); list.add(entity); } DaoUtil.BASE.addList(list); SysDemo entity=new SysDemo(); entity.setCode("456456456456456456456456456456456456456456456456456456456456456456456456456456456456456456456456456456456456456456456456456456456456456456456456456456456456456456456456456"); entity.setName("小明"); entity.setTimeUnit(TimeUnitEnum.SECOND); DaoUtil.BASE.add(entity); }catch(Throwable e){ log.error("Error occured, cause by: {}",e.getMessage()); transactionStatus.setRollbackOnly(); } return null; } }); } ```