Skip to content

介绍

模块基于mybatisplus框架做增强,拥有着强大性能的同时又不失简单灵活,特性如下:

  • SQL封装:提供自定义Sql的封装,方便使用
  • 动态Mapper:可以不编写Mapper,通过实体自动生成Mapper减少代码量
  • CRUD增强:提供CRUD增强接口
  • 数据审计:记录对数据的修改和删除操作
  • 数据审计:提供自动填充注解
  • 数据权限:支持各种自定义数据权限的实现,并有基于部门数据权限的默认实现

此模块还集成了stream-plugin-mybatis-plus和mybatis-plus-join-boot-starter两个mybatisplus增强库用来简化数据库操作,具体使用方式参见相关官网

依赖库

名称描述
mybatis-plus-boot-starter
stream-plugin-mybatis-plus
mybatis-plus-join-boot-starter
druid-spring-boot-starter
hutool-all
flyway-core
auto-service
slf4j-api
javers-core
guava
knife4j-openapi2-spring-boot-starter

快速开始

引入

xml
<dependency>
		<groupId>io.github.zhanghongbin</groupId>
		<artifactId>zebra-spring-boot-starter-mybatis-plus</artifactId>
</dependency>

MybatisPlusInterceptor 拦截器

默认已经增加了 OptimisticLockerInnerInterceptor,PaginationInnerInterceptor,并提供 MyBatisPlusUtil.addInterceptor 方法增加拦截器。

SortQuery 排序查询类

所有不带分页排序需要继承此抽象类,用来规范化参数名称。QueryHelper 类可以根据参数自动生成 QueryWrapper 对象,此类位于 zebra-common-util 模块中的 org.zebra.common.core 包里。
SortQuery 包括两个属性分别为:

名称描述
String sort排序列
String order排序的方向desc或者asc

支持的用法如下: > * {isAsc:"asc",orderByColumn:"id"} order by id asc > * {isAsc:"asc",orderByColumn:"id,createTime"} order by id asc,create_time asc > * {isAsc:"desc",orderByColumn:"id,createTime"} order by id desc,create_time desc > * {isAsc:"asc,desc",orderByColumn:"id,createTime"} order by id asc,create_time desc

PageQuery 抽象分页查询类

所有分页查询对象的基类,只要是带条件的分页查询都需要继承此类,此类并继承了 SortQuery,此类位于 zebra-common-util 模块中的 org.zebra.common.core 包里, 查询参数:

名称描述
Integer size分页大小
Integer page当前页数
String sort排序列
String order排序的方向desc或者asc

支持的用法如下: > * {isAsc:"asc",orderByColumn:"id"} order by id asc > * {isAsc:"asc",orderByColumn:"id,createTime"} order by id asc,create_time asc > * {isAsc:"desc",orderByColumn:"id,createTime"} order by id desc,create_time desc > * {isAsc:"asc,desc",orderByColumn:"id,createTime"} order by id asc,create_time desc

MPageQuery 分页查询类

mybatisplus 分页查询需要使用MPageQuery 此类继承PageQuery 示例:

java

@GetMapping("/get1")
public String get(MPageQuery userPageQuery) {
		//构建分页对象
		IPage page = userPageQuery.build();
		return "test";
}

Mapper增强

(注:如果继承ServicePlusImpl或DynamicServicePlusImpl类,直接略过)

Mapper增强类 org.zebra.mybatis.plus.core.MapperPlus 整合了开源库stream-query的IMapper和 mybatis-plus-join-boot-starter 的 MPJBaseMapper 并增加了sql处理的能力,通常情况下可以有两种方式使用Mapper。

  1. 继承org.zebra.mybatis.plus.core.MapperPlus,MapperPlus 里所有的接口都可以使用。
  2. 编写实体后,不编写 Mapper 类的话,会根据实体自动生成 Mapper,这就是所谓的动态Mapper,动态 Mapper 也继承了 MapperPlus 接口,里边所有接口都可以使用。

MapperPlus 增强了Sql处理接口分别为: 只描述动态生成Mapper如何使用,第一种使用为正常使用Mapper方法。

  1. 首先创建数据库表
sql
CREATE TABLE "public"."user_test1"
(
		"id"   int4,
		"name" varchar(255);
)
  1. 配置springboot yml 文件,修改相关参数
yml
spring:
	datasource:
		driver-class-name: org.postgresql.Driver
		url: jdbc:postgresql://${DB_HOST:36.138.170.195}:${PG_DB_PORT:15432}/${PG_DB_NAME:yl_gis_test2}?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8&stringtype=unspecified
		username: ${PG_DB_USERNAME:postgres}
		password: ${PG_DB_PASSWORD:jinma}
  1. 创建实体类
java

@Data
@TableName("public.user_test1")
public class UserLog {
		private Integer id;
		private String name;
}
  1. 创建DTO
java

@Data
public class UserDto {
		private Integer id;
		private String name;
}
java
/**
 * 返回多条结果
 *
 * @param sql
 * @return
 */
List<Map<String, Object>> selectListBySqlPlus(String sql, Map<String, Object> params);

//使用Database.execute 方法 第一个参数为实体,第二个为函数参数,参数化占位必须params.属性 例如 #{params.id}
@Test
void selectListBySqlPlusTest() {
		Map<String, Object> params = new HashMap<>();
		params.put("id", 3);
		List<Map<String, Object>> userDtoList = Database.execute(UserLog.class, (MapperPlus<List<Map<String, Object>>> baser) ->
						baser.selectListBySqlPlus("select * from user_test1 where id =#{params.id}", params)
		);
		System.out.print(userDtoList.size());
}
java
/**
 * 返回单条结果
 *
 * @param sql
 * @return
 */
Map<String, Object> selectOneBySqlPlus(String sql, Map<String, Object> params);

@Test
void selectOneBySqlPlusTest() {
		Map<String, Object> params = new HashMap<>();
		params.put("id", 31);
		//使用Database.execute 方法 第一个参数为实体,第二个为函数参数,参数化占位必须params.属性 例如 #{params.id}
		Map<String, Object> user = Database.execute(UserLog.class, (MapperPlus<Map<String, Object>> baser) ->
						baser.selectOneBySqlPlus("select * from user_test1 where id =#{params.id} limit 1", params)
		);
		System.out.print(user);
}

/**
 * 返回分页
 *
 * @param page
 * @param sql
 * @param params
 * @return
 */

Page<Map<String, Object>> selectPageBySql(IPage page, String sql);

/**
 * 参数化分页
 */
@Test
void selectPageBySqlPlusTest() {
		MPageQuery query = new MPageQuery();
		query.setPageSize(10);
		query.setPageNum(1);
		Map<String, Object> params = new HashMap<>();
		params.put("id", 1);
		//使用Database.execute 方法 第一个参数为实体,第二个为函数参数,参数化占位必须params.属性 例如 #{params.id}
		/**
		 * mybatis xml 配置都支持,条件,循环等
		 * select * from user_test1  <if test="params.id== 1">where id=#{params.id} </if>
		 */
		IPage<Map<String, Object>> userPage = Database.execute(UserLog.class, (MapperPlus<Map<String, Object>> baser) ->
						baser.selectPageBySqlPlus(query.build(), "select * from user_test1 where id =#{params.id} ", params)
		);
		System.out.println(userPage);
}

// 修改
public int updateBySqlPlus(String sql, Map<String, Object> params);

@Test
void updateBySqlPlus() {
		Map<String, Object> params = new HashMap<>();
		params.put("id", 3);
		params.put("name", "xxx");
		int n = Database.execute(UserLog.class, (MapperPlus<UserLog> baser) ->
						baser.updateBySqlPlus("update user_test1 set name=#{params.name} where id =#{params.id}", params)
		);
		System.out.println(n);
}

// 删除
public int deleteBySqlPlus(String sql, Map<String, Object> params);

@Test
void deleteBySqlPlus() {
		Map<String, Object> params = new HashMap<>();
		params.put("id", 1);
		int n = Database.execute(UserLog.class, (MapperPlus<UserLog> baser) ->
						baser.deleteBySqlPlus("delete from user_test1  where id =#{params.id}", params)
		);
		System.out.println(n);
}

//增加
public int insertBySqlPlus(String sql, Map<String, Object> params);

@Test
void insertBySqlPlus() {
		Map<String, Object> params = new HashMap<>();
		params.put("id", 1);
		params.put("name", "test");
		int n = Database.execute(UserLog.class, (MapperPlus<UserLog> baser) ->
						baser.insertBySqlPlus("insert into  user_test1  values(#{params.id},#{params.name})", params)
		);
		System.out.println(n);
}

在拼写sql操作建议使用 MyBatis 中提供了一个SQL工具类, org.apache.ibatis.jdbc.SQL。

java

@Test
public void sqlTest() {
		SQL sql = new SQL().SELECT("id", "name").FROM("user_test1").WHERE("id=1");
		System.out.println(sql.toString());
}

@Test
public void testDynamicSQL() {
		String sql = new SQL() {{
				SELECT("P.ID, P.USERNAME, P.PASSWORD");
				SELECT("P.FIRST_NAME, P.LAST_NAME");
				FROM("PERSON P");
				if (id != null) {
						WHERE("P.ID = #{id}");
				}
				if (firstName != null) {
						WHERE("P.FIRST_NAME = #{firstName}");
				}
				if (lastName != null) {
						WHERE("P.LAST_NAME = #{lastName}");
				}
				ORDER_BY("P.LAST_NAME");
		}}.toString();
}

Service 增强

提供两个 Service 抽象类分别为:

  1. ServicePlusImpl
  2. DynamicServicePlusImpl

一般情况下自定义的 Service 需要实现 ServicePlusImpl 或 DynamicServicePlusImpl 来使用 mybatisplus,stream-query,join 提供的接口。 如果是动态 Mapper 需要继承 DynamicServicePlusImpl,自定义 Mapper 需要继承 ServicePlusImpl,这两个类提供的所有接口都是一致的,使用方式也具有一致性。 使用方式如下:

java
/**
 * 动态 Mapper
 */
@Service
public class UserTestServiceImpl extends DynamicServicePlusImpl<UserLog> {
		public void test2() {
		}
}

/**
 * 自定义 Mapper
 */
@Service
public class SysAppVersionServiceImpl extends ServicePlusImpl<SysAppVersionMapper, SysAppVersionEntity> {
		public void test1() {

		}
}

ServicePlusImpl 和 DynamicServicePlusImpl 接口列表

名称描述
protected final boolean transactional(Consumer<IServicePlus<? extends MapperPlus<T>, T>> consumer)手动事务处理
protected final QueryWrapper<T> queryWrapper()参见 mybatisplus 使用 构建普通QueryWrapper对象
protected final UpdateWrapper<T> updateWrapper()参见 mybatisplus 使用 构建普通UpdateWrapper对象
protected final LambdaQueryWrapper<T> lambdaQueryWrapper()参见 mybatisplus 使用 构建LambdaQueryWrapper对象
protected final LambdaUpdateWrapper<T> lambdaUpdateWrapper()参见 mybatisplus 使用 构建lambdaUpdateWrapper对象
protected final MPJLambdaWrapper<T> mpjLambdaQueryWrapper()参见 mybatis-plus-join-boot-starter 使用
protected final MPJQueryWrapper<T> mpjQueryWrapper()参见 mybatis-plus-join-boot-starter 使用
protected final UpdateJoinWrapper<T> mpjUpdateWrapper()参见 mybatis-plus-join-boot-starter 使用
protected final DeleteJoinWrapper<T> mpjDeleteWrapper()参见 mybatis-plus-join-boot-starter 使用
servicePlus核心字段

transactional接口使用,参数为匿名函数

java

@Service
public class UserTestServiceImpl extends DynamicServicePlusImpl<UserLog> {
		public void test2() {
				this.transactional(() -> {
						UserLog userLog = new UserLog();
						userLog.setId(100);
						userLog.setName("fff");
						this.servicePlus.save(userLog);
						this.servicePlus.deleteBySqlPlus("delete from user_test1 where aid=1", new HashMap<>());
				});
		}
}

servicePlus 属性接口列表

描述接口参数
获取单条数据public <DTO, Q> DTO findOne(Q query, Class<DTO> dtoClass)参数1:<br/> Q 可以为Query对象或者是Dto 内部会转换成QueryWrapper对象。可以结合@Query注解使用,具体详见Query使用<br/>参数2:<br/> dtoClass 返回的自定义dto
获取多条数据public <DTO, Q> List<DTO> findList(Q query, Class<DTO> dtoClass)参数1:<br/> Q 可以为Query对象或者是Dto 内部会转换成QueryWrapper对象。可以结合@Query注解使用,具体详见Query使用<br/> 参数2:<br/> dtoClass 返回的自定义dto
获取分页数据public <DTO, Q extends PageQuery> Page<DTO> findPage(Q query, Class<DTO> dtoClass)参数1:<br/> Q 必须是PageQuery对象或子类可以结合@Query注解使用,具体详见Query使用<br/>参数2:<br/> dtoClass 返回的自定义dto
根据sql获取多条数据public <DTO, P> List<DTO> findListBySqlPlus(Class<DTO> dtoClass, String sql, P params)参数1:<br/> dtoClass 返回的自定义dto <br/>参数2:<br/> sql<br/> 参数3:<br/> 参数
根据sql获取单条数据public <DTO, P> DTO findOneBySqlPlus(Class<DTO> dtoClass, String sql, P params)参数1:<br/> dtoClass 返回的自定义dto <br/>参数2:<br/> sql<br/> 参数3:<br/> 参数
根据sql获取分页数据public <DTO, P> Page<DTO> findPageBySqlPlus(Class<DTO> dtoClass, IPage page, String sql, P params)参数1:<br/> dtoClass 返回的自定义dto <br/>参数2:<br/> 分页对象<br/> <br/>参数3:<br/> sql<br/> 参数4:<br/> 参数
根据sql更新数据public <P> int updateBySqlPlus(String sql, P params)参数1:<br/> sql <br/>参数2:<br/> 参数
根据sql删除数据public <P> int deleteBySqlPlus(String sql, P params)参数1:<br/> sql <br/>参数2:<br/> 参数
根据sql增加数据public <P> int saveBySqlPlus(String sql, P params)参数1:<br/> sql <br/>参数2:<br/> 参数可以是Map或者是对象
根据对象条件删除数据public <Q> boolean deleteByQuery(Q query)参数:<br/> Q 可以为Query对象或者是Dto 内部会转换成QueryWrapper对象。可以结合@Query注解使用,具体详见Query使用
根据实体删除数据public <T, R> boolean deleteByEntity(T t, Func1<T, R>... funcs)参数1:<br/> T 实体对象 <br/>参数2:<br/> 需要过滤的实体属性

示例1. 一对多查询 相关类如下:

字段
实体类 UserEntityInteger id<br/>String name
实体类 AddressEntityInteger id<br/>Integer userId<br/>String address
dto类 UserDTOInteger id<br/>String name<br/>List< AddressDTO > addressList
dto类 AddressDTOInteger id<br/>Integer userId<br/>String addrss

全部映射,把数据库实体类AddressDO所有属性全部映射到 UserDTO 的 addressList 列表中 .selectCollection(AddressEntity.class, UserDTO::getAddressList)

java

@Slf4j
@Service
public class UserTestServiceImpl extends DynamicServicePlusImpl<UserEntity> {
		public void testOneToMany() {
				//和Mybatis plus一致,MPJLambdaWrapper的泛型必须是主表的泛型,并且要用主表的Mapper来调用
				MPJLambdaWrapper<UserEntity> mpjLambdaWrapper = this.mpjLambdaQueryWrapper();
				mpjLambdaWrapper.selectAll(UserEntity.class).selectCollection(AddressEntity.class,
												UserDTO::getAddressList)
								.leftJoin(AddressEntity.class, AddressEntity::getUserId, UserEntity::getId);
				List<UserDTO> userDTOList = this.servicePlus.selectJoinList(UserDTO.class, mpjLambdaWrapper);
				System.out.println(userDTOList);
		}
}
sql
SELECT t.id, t.name, t1.id AS joina_id, t1.address, t1.user_id
FROM public.user_test1 t
				LEFT JOIN public.address t1 ON (t1.user_id = t.id)

img_6.png

2. 一对一查询

字段
实体类 UserEntityInteger id<br/>String name
实体类 AddressEntityInteger id<br/>Integer userId<br/>String address
dto类 UserDTOInteger id<br/>String name<br/> AddressDTO address
dto类 AddressDTOInteger id<br/>Integer userId<br/>String address
java

@Slf4j
@Service
public class UserTestServiceImpl extends DynamicServicePlusImpl&lt;UserEntity&gt; {
		public void testOneToOne() {
				MPJLambdaWrapper&lt;UserEntity&gt; mpjLambdaWrapper = this.mpjLambdaQueryWrapper();
				mpjLambdaWrapper.selectAll(UserEntity.class)
								.selectAssociation(AddressEntity.class, UserDTO::getAddress)
								.innerJoin(AddressEntity.class, AddressEntity::getUserId, UserLog::getId);
				List&lt;UserDTO&gt; userDtoList = this.servicePlus.selectJoinList(UserLogDto.class, mpjLambdaWrapper);
				System.out.println(userDtoList);
		}
}
sql
SELECT t.id, t.name, t1.id AS joina_id, t1.address, t1.user_id
FROM public.user_test1 t
				INNER JOIN public.address t1 ON (t1.user_id = t.id)

img_7.png

3. 联表查询返回简单实体<>

字段
实体类 UserEntityInteger id<br/>String name
实体类 AddressEntityInteger id<br/>Integer userId<br/>String address
dto类 UserDTOInteger id<br/>String name<br/> String address <br/> Integer addressId
java

@Slf4j
@Service
public class UserTestServiceImpl extends DynamicServicePlusImpl<UserEntity> {
		public void testOneToOne() {
				MPJLambdaWrapper<UserEntity> mpjLambdaWrapper = this.mpjLambdaQueryWrapper();
				mpjLambdaWrapper.selectAll(UserEntity.class)
								.selectAssociation(AddressEntity.class, UserDTO::getAddress)
								.innerJoin(AddressEntity.class, AddressEntity::getUserId, UserLog::getId);
				List<UserDTO> userDtoList = this.servicePlus.selectJoinList(UserLogDto.class, mpjLambdaWrapper);
				System.out.println(userDtoList);
		}
}
sql
SELECT t.id, t.name, t1.address, t1.id AS addressId
FROM public.user_test1 t
				INNER JOIN public.address t1 ON (t1.user_id = t.id)

img_8.png

相关文档 https://mybatisplusjoin.com/pages/quickstart/js.html

Controller 增强

提供抽象类 CrudController 简化基本的 crud 操作,使用者只需要继承 CrudController 即可,CrudControoler有5个泛型参数分别为:

  • S service具体实现
  • Q 以Query结尾的vo对象
  • C 以Command结尾的vo对象
  • P MPageQuery 类或子类 vo对象
  • R 以Result结尾的vo对象

泛型参数不能直接使用实体,如果为了简化可以只使用Query结尾的vo对象,具体视情况而定。详见技术规范中的java项目规范

java

@RestController
@RequestMapping("/iam/baseClasses")
@Api(tags = "班组班别接口")
public class BaseClassesController extends CrudController<BaseClassesServiceImpl,BaseClassesQuery,BaseClassesQuery,BaseClassesPageQuery,BaseClassesQuery> {

}

以下为默认提供的接口

描述方法类型url参数
增加数据POSTjsonhttp://localhost:9090/basic/user实体参数
修改数据Putjsonhttp://localhost:9090/basic/user实体参数
查询单条数据GETurlhttp://localhost:9090/basic/user/idid
查询所有数据GETurlhttp://localhost:9090/basic/user/list无参数查询所有
有实体参数为等于的条件查询
根据id删除数据DELETEurlhttp://localhost:9090/basic/user/idid
根据多个id删除数据DELETEurlhttp://localhost:9090/basic/user?idsids 多个用,分割
分页查询GETurlhttp://localhost:9090/basic/user/page每页记录数size
当前页page
排序字段sort
排序 order desc,asc
详见 MPageQuery 使用

如果需要再默认接口上增加操作,如日志记录或者权限控制等,需要实现以下两个方法,方法执行前before,方法执行后after,通过method.getName()能够区分执行的哪个方法,进行区别化处理。

java

@RestController
@RequestMapping("/iam/baseClasses")
@Api(tags = "班组班别接口")
public class BaseClassesController extends CrudController<BaseClassesServiceImpl,BaseClassesQuery,BaseClassesQuery,BaseClassesPageQuery,BaseClassesQuery> {
    
	@Override
    protected void before(Method method, Object params) {
  
    }

    @Override
    protected void after(Method method, Object params, Object result) {
      
    }

}

审计泛型实体(AuditedEntity)

审计泛型实体提供 createBy,updateBy,createTime,updateTime字段,createBy,updateBy为泛型类型,updateBy,createTime为 Date类型, 数据库表中也必须存在以上字段,用户定义的实体可以继承审计实体,并配合 AuditedMetaObjectHandler 使用 ,用来实现自动对以上字段进行赋值,无需使用者主动进行操作,示例:
首先需把 AuditedMetaObjectHandler 对象增加到 MetaObjectHandlerPlus 中,AuditedMetaObjectHandler 构造函数需要 Supplier 对象来获取用户id,然后 SysClientEntity 实体继承 AuditedEntity 如果用户id为字符串那泛型类型为String。

java

@Configuration
public class AdminConfig {

		public AdminConfig(MetaObjectHandlerPlus metaObjectHandlerPlus) {
				metaObjectHandlerPlus.addMetaObjectHandler(new AuditedMetaObjectHandler(() ->
								LoginHelper.getLoginUser().getUserId()
				));
		}
}

@TableName("sys_client")
public class SysClientEntity extends AuditedEntity<String> {

		/**
		 * id
		 */
		@TableId(value = "id")
		private Long id;
}

数据审计

监控与记录数据库的变化是非常必要的操作,特别是当出现数据异常时,可以通过审计日志追溯数据变化的具体情况。
数据审计功能和行为日志是紧密关联再一起的,必须先启用行为日志,以下是开启数据审计相关配置。 yml配置: enabled 是否启用默认为 false ignored-columns 忽略表或者列不进行审计记录,多个表用分号分隔规则如下:

  1. talbe1.column1,column2 : 表示忽略这个表的这2个字段
  2. table2.*: 表示忽略这张表的INSERT/UPDATE,delete暂时还保留
yml
zebra:
  database:
    audit:
	  enabled: false
	  ignored-columns: table1.c1,c2;table2.*
  1. 字段名称如果要显示中文需要在实体上增加@ApiModelProperty注解 示例:
java

@Data
@TableName("public.user_test1")
public class UserLog {
		@ApiModelProperty("id1")
		private Integer id;

		@ApiModelProperty("名称")
		private String name;

		@ApiModelProperty("年龄")
		private String userAge;
}
  1. 如果想要更友好的显示某些值的含义,如数据库字段sex 1代表男,2代表女,需要实现以下接口,并注入到spring容器中
java
public interface DataAuditFieldTranslation {

		/**
		 * 数据翻译
		 *
		 * @param tableName 表名
		 * @param columnName 字段名称
		 * @param columnValue 字段值
		 * @return
		 */
		public String translate(String tableName, String columnName, String columnValue);
}

示例:

java

public class TestDataAuditFieldTranslation implements DataAuditFieldTranslation {

		@Override
		public String translate(String tableName, String columnName, String columnValue) {
				if (columnValue.equals("1")) {
						return "男";
				} else {
						return "女";
				}
		}
}

@Configuration
public class TestConfig {

		@Bean
		public DataAuditFieldTranslation dataAuditFieldTranslation() {
				return new TestDataAuditFieldTranslation();
		}
}

表结构

名称描述数据示例
id
log_id行为日志id
table_name表名
change_data变化数据
title详情新增: 年龄新值为:666 id1新值为:920 名称新值为:111 
修改: 名称原值为:111 新值为:111 年龄原值为:666 新值为:666 
删除: id1原值为:90 名称原值为:111 年龄原值为:666 
type操作类型(1新增 2修改 3删除)
create_time操作时间

数据审计结果会通过 spring event 发送出来,具体示例如下:

java
package org.zebra.admin.common.log;

import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.db.Db;
import cn.hutool.db.Entity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import org.zebra.satoken.LoginHelper;
import org.zebra.satoken.LoginUser;

import javax.sql.DataSource;
import java.util.concurrent.ExecutorService;

@Component
public class OperationLogHandler {

    @Autowired
    private DataSource dataSource;

    private ExecutorService executorService = ThreadUtil.newExecutor(3);

    // @EventListener(condition = "#operLogEvent instanceof T(org.zebra.logging.event.OperLogEvent)")
    @EventListener
    public void addOperationLog(org.zebra.logging.event.OperLogEvent operLogEvent) {
        LoginUser<String, String> loginUser = LoginHelper.getLoginUser();
        if (loginUser != null) {
            executorService.execute(() -> {
				//设置行为日志内容
                operLogEvent.setUserId(loginUser.getUserId());
                operLogEvent.setUserName(loginUser.getUserName());
                try {
                    Entity userDeptEntity = Db.use(dataSource)
                            .queryOne(
                                    "select a1.nick_name,a2.dept_name from sys_user as a1,sys_dept as a2 where a1.dept_id = a2.dept_id and a1.user_id = ? ",
                                    loginUser.getUserId());
                    if (userDeptEntity != null && !userDeptEntity.isEmpty()) {
                        operLogEvent.setDeptName(userDeptEntity.getStr("dept_name"));
                        operLogEvent.setNickName(userDeptEntity.getStr("nick_name"));
                    }
                    Entity entity = Entity.create("sys_operation_log")
                            .parseBean(operLogEvent, true, true)
                            .set("tenant_Id", loginUser.getTenantId());
                    //获取审计日志数据
                    List<OperLogEvent.AuditData> auditDataList = operLogEvent.getAuditDataList();
					//todo 
					//保存操作
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }
    }
}

自动填充

可以在对数据库做插入或更新操作的时候,自动赋值数据操作人、操作时间、默认值等。
注意: 如果本地已经实现了com.baomidou.mybatisplus.core.handlers.MetaObjectHandler,将会导致框架的MetaObjectHandlerPlus无法生效,进而将会产生注解无法工作的情况,请注意! 因为mybatisplus 只支持一个MetaObjectHandler实例存在,如果想自定义自己的MetaObjectHandler,同时还想用框架本身的自动填充功能,需要自己实现MetaObjectHandler接口,并注册到MetaObjectHandlerPlus里。
示例:
通过调用 MetaObjectHandlerPlus 实例的addMetaObjectHandler方法进行注册,构造 MetaObjectHandlerPlus 方法有很多种,下边只是其中一种

java

@Configuration
public class TestConfig {

		public TestConfig(MetaObjectHandlerPlus metaObjectHandlerPlus) {
				metaObjectHandlerPlus.addMetaObjectHandler(null);
		}
}

注解介绍 @FillTime 自动赋值数据操作时间。需结合mybatis-plus原框架注解@TableField (该注解的使用请查看官方文档)一并使用才有效。 被标注的字段,在可允许的类型范围(String、Long、long、Date、LocalDate、LocalDateTime)内,数据被操作的情况下,会自动被赋值上当前时间。 如果使用String的话需要同时指明format参,用以确认格式化后的样式

属性类型必需默认值描述
formatString非必需yyyy-MM-dd HH:mm:ss如果字段类型为String,需要制定字符串格式

@FillData 指定实现方式,自动赋值数据操信息。需结合mybatis-plus原框架注解@TableField (该注解的使用请查看官方文档)一并使用才有效。 被标注的字段,会根据@FillData中AuditHandler的实现来返回对应的值。 通常的实现方案都是用户信息(id、name等)放入header中,全局定义函数来获取

属性类型必需默认值描述
valueClass>必需yyyy-MM-dd HH:mm:ss自定义用户生成方式

@FillValue 数据插入的时候字段的默认值,支持类型:String, Integer, int, Long, long, Boolean, boolean, Double, double, Float, float, BigDecimal, Date, LocalDate, LocalDateTime, 枚举(仅支持枚举的名字作为默认值)

属性类型必需默认值描述
valueString必需默认值
formatboolean非必需yyyy-MM-dd HH:mm:ss如果字段类型为时间类型(Date,LocalDateTime等),需要制定字符串格式

示例:

java

@Data
@TableName("public.user_test1")
public class UserLog {
		@ApiModelProperty("id1")
		private Integer id;

		@ApiModelProperty("名称")
		private String name;

		@ApiModelProperty("年龄")
		private String userAge;

		@TableField(fill = FieldFill.INSERT_UPDATE)
		@FillTime
		private Date createTime;

		@TableField(fill = FieldFill.UPDATE)
		@FillValue("111")
		private String color;

		@TableField(fill = FieldFill.INSERT)
		@FillData(TestHandler.class)
		private Integer sex;
}

@Component
public class UserSexHandler implements AutoFillHandler<Integer> {

		@Override
		public Integer getVal(Object object, Class clazz, Field field) {
				return 1;
		}
}

数据冗余

为了避免高频的数据关联查询,一种方案是做数据冗余,将其他表的部分字段冗余到当前表。但是这个方案牵扯一个数据修改后如何同步的问题,本功能就是为了解决这个问题而生的。 注意: 1、高频的写操作的数据,不建议被冗余,因为会带来数据库很大的负担。 2、不要把此功能当做银弹,更多的是应该思考是否有必要冗余,冗余了应不应该更新。 注解介绍 @RedundancyField 通过注解指定冗余字段,底层框架自动通过mybatis Interceptor 对修改进行拦截后获取实体内容并通过 Spring中的事件机制监听 EntityUpdateEvent 事件,完成数据自动更新。 分布式情况下如何同步其他服务的数据_? 参见 同步冗余字段 cdc 功能模块

属性类型是否必须默认值描述
sourceClass<?>Void.class数据来源的Entity class
fieldString数据来源的Entity对应的属性
conditionsCondition[]被关联的Entity所需要的条件
updateConditionCondition[]通常指被更新表自身的特殊条件,例如:enable=1 and is_deleted=0

@Condition 冗余字段的关联条件

属性类型是否必须默认值描述
selfFieldString关联数据来源Entity所需的自身字段
sourceFieldString"id"数据来源的Entity的字段,默认为id

示例: 假设地址表上需要冗余用户名名称,如果用户的名字有改动,则需要同步新的改动,代码如下:

java

@Data
@TableName("public.user_test1")
public class User {
		@ApiModelProperty("id1")
		private Integer id;

		@ApiModelProperty("名称")
		private String name;

		@ApiModelProperty("年龄")
		private String userAge;
}

@Data
@TableName("public.address")
public class AddressEntity {

		private Integer id;
		private String address;
		private Integer userId;

		@RedundancyField(source = User.class, field = "name",
						conditions = @Condition(selfField = "userId"))
		private String userName;
}

数据权限

数据权限,就是根据不同角色的登录用户,查看不同的数据。 框架实现了通用数据权限的方法,不关心表结构,业务,rpc等细节,具体使用方式如下: 在 mapper 的方法上使用 DataPermission 注解,并指定 value 属性, value 为 DataPermissionRule 接口的实现类,原理对 sql 进行拦截并改写 where 条件。 示例:

java

@Mapper
public interface UserTestMapper extends MapperPlus<UserLog> {

		@DataPermission(value = TestDataPermissionRule.class)
		@SelectProvider(type = UserTestMapper.g1.class, method = "myPage")
		public UserLog testA(@Param(Constants.WRAPPER) Wrapper<UserLog> wrapper);

		class g1 {
				public String myPage(@Param(Constants.WRAPPER) Wrapper<UserLog> wrapper) {
						return "select * from user_test1 ${ew.customSqlSegment} limit 1";
				}
		}
}

DataPermissionRule 接口为数据权限规则接口,分别有两个方法:

  1. Expression getExpression(Method method) 获取自定义的条件,使用者需要实现此接口,参数为调用的 mapper方法,返回的是 jsqlparser 开源库提供的条件对象,expression 其实转换成字符串格式就是 dept_id = 1 或 user_id = 2 类似这种条件,有可能为多个。
  2. default Expression parse(String sql) throws Exception 可以根据字符串返回条件对象,使用者不需要实现此接口

需要注意的是实现此接口的类必须注解 @Component 交给 spring 容器进行管理,以下具体实现

java

@Component
public class TestDataPermissionRule2 implements DataPermissionRule {

		@Override
		public Expression getExpression(Method method) {
				return null;
		}
}

以上数据权限接口和注解只是一个模板,没有具体实现,以下实现了一个比较常见的基于角色的数据权限规则 通常情况下有以下5种类型

  1. 全部数据权限,表示拥有所有部门的数据权限 ·
  2. 自定数据权限,表示拥有指定的若干部门的数据权限
  3. 本部门数据权限,表示仅拥有用户所属部门(不包括子部门)的数据权限
  4. 本部门及以下数据权限,表示仅拥有用户所属部门和所有子部门的数据权限
  5. 仅本人数据权限,表示仅拥有用户本人的数据权限

同时后台管理界面上也需要支持数据权限的操作如下:

img_12.png

img_11.png

使用 DeptDataPermissionField 注解此注解要和 DataPermission 注解联合使用,一起注解到 mapper 方法上,并提供以下属性:

属性描述
userIdFieldName数据库中,用户id字段,默认为 user_id
userIdFieldType数据库中,用户id类型,默认为 FieldType.NUMBER
deptIdFieldName数据库中,部门id字段,默认为 dept_id
deptIdFieldType数据库中,部门id类型,默认为 FieldType.NUMBER

编写类继承 DeptDataPermissionRule 抽象类,并实现以下方法:

java
/**
 * 根据角色id获取所有部门id
 */
protected abstract Set<String> getDeptIdByCustom(String roleId);

/**
 * 根据部门id获取当前和所有下级部门的id
 */
protected abstract Set<String> getDeptAndChildId(String deptId);

/**
 * 获取当前用户权限部门信息
 *
 * @return
 */
protected abstract DataPermissionCurrentUser getDataPermissionCurrentUser();

DataPermissionCurrentUser 类结构如下,需要把值填充完成。

java

@Data
public class DataPermissionCurrentUser {

		/**
		 * 用户 ID
		 */
		private String userId;

		/**
		 * 角色列表
		 */
		private Set<Role> roles;

		/**
		 * 部门 ID
		 */
		private String deptId;

		/**
		 * 当前用户角色信息
		 */
		@Data
		@AllArgsConstructor
		public static class Role {

				/**
				 * 角色 ID
				 */
				private String roleId;

				/**
				 * 数据权限
				 */
				private Integer dataScope;
		}
}

其中 dataScope 定义常量值为:

  1. 全部数据权限 1
  2. 部门数据权限 2
  3. 部门及以下数据权限 3
  4. 仅本人数据权限 4
  5. 自定数据权限 5

DeptDataPermissionRule 实现类示例:

java

@Component
public class TestDataPermissionRule extends DeptDataPermissionRule {
		@Override
		protected DataPermissionCurrentUser getDataPermissionCurrentUser() {
				DataPermissionCurrentUser dataPermissionCurrentUser = new DataPermissionCurrentUser();
				dataPermissionCurrentUser.setUserId("1");
				dataPermissionCurrentUser.setDeptId("22");
				DataPermissionCurrentUser.Role role = new DataPermissionCurrentUser.Role("3", 2);
				Set<DataPermissionCurrentUser.Role> roles = new HashSet<>();
				roles.add(role);
				dataPermissionCurrentUser.setRoles(roles);
				return dataPermissionCurrentUser;
		}

		@Override
		protected Set<String> getDeptIdByCustom(String roleId) {
				Set<String> ids = new HashSet<>();
				ids.add("1");
				ids.add("2");
				return ids;
		}

		@Override
		protected Set<String> getDeptAndChildId(String deptId) {
				Set<String> ids = new HashSet<>();
				ids.add("1");
				ids.add("2");
				return ids;
		}
}

接下来在 Mapper 上进行注解,DeptDataPermissionField 字段名称跟自定义的sql有关系,如有表别名,就需要把表别名也加上

java

@DataPermission
@DeptDataPermissionField(userIdFieldName = "d.user_id", deptIdFieldName = "r.dept_id")
Page<SysRoleVo> selectPageRoleList(@Param("page") Page<SysRole> page, @Param(Constants.WRAPPER) Wrapper<SysRole> queryWrapper);
xml

<select id="selectPageRoleList" resultMap="SysRoleResult">
		select distinct r.role_id,
		r.role_name,
		r.role_key,
		r.role_sort,
		r.data_scope,
		r.menu_check_strictly,
		r.dept_check_strictly,
		r.status,
		r.del_flag,
		r.create_time,
		r.remark
		from sys_role r
		left join sys_user_role sur on sur.role_id = r.role_id
		left join sys_user u on u.user_id = sur.user_id
		left join sys_dept d on u.dept_id = d.dept_id
		${ew.getCustomSqlSegment}
</select>

以下为 service 构建 wrapper 示例

java private
		Map<String, Object> params = bo.getParams();
		QueryWrapper<SysRole> wrapper = Wrappers.query();
		wrapper.eq("r.del_flag", UserConstants.ROLE_NORMAL)
						.eq(ObjectUtil.isNotNull(bo.getRoleId()), "r.role_id", bo.getRoleId())
						.like(StringUtils.isNotBlank(bo.getRoleName()), "r.role_name", bo.getRoleName())
						.eq(StringUtils.isNotBlank(bo.getStatus()), "r.status", bo.getStatus())
						.like(StringUtils.isNotBlank(bo.getRoleKey()), "r.role_key", bo.getRoleKey())
						.between(params.get("beginTime") != null && params.get("endTime") != null,
										"r.create_time", params.get("beginTime"), params.get("endTime"))
						.orderByAsc("r.role_sort").orderByAsc("r.create_time");
		return wrapper;
}

以上基于是部门数据权限的一个实现,可以参考实现方式进行自定义数据权限规则来满足不同业务需求。


druid 配置

druid提供了可视化的监控方案以下是简化 druid 监控配置

  1. enabled 为是否启用 默认为true
  2. aop-patterns 主要用来监控SpringBean 默认为当前工程的包路径,可以不进行配置 示例 com.ninemax.example.*
  3. slow-sql-millis 慢sql记录 单位毫秒 可以不进行配置
  4. username web console 登录名 可以不进行配置则不需要登录名
  5. password web console 密码 可以不进行配置则不需要密码
  6. allow web console 访问控制,不进行配置则访问不进行限制,配置127.0.0.1只对本机访问开发
yml
zebra:
	database:
		monitor:
			enabled: true
			aop-patterns:
			slow-sql-millis:
			username:
			password:
			allow:

web 管理控制台 img_3.png

字段常量

某个地方的代码想使用下领域字段的名称,但是又不想写死一个字符串(编译期不可校验)。 使用 @DomainConstantGen 注解 maven 编译后自动生成字段常量代码,生成策略为 在所在类的包下gen目录中并且以Gen为后缀。

java

@Data
@DomainConstantGen
@TableName("public.user_test1")
public class UserLog {
		@ApiModelProperty("id1")
		private Integer id;

		@ApiModelProperty("名称")
		private String name;

		@ApiModelProperty("年龄")
		@TableField("user_age")
		private String userAge;

		@TableField(fill = FieldFill.INSERT_UPDATE)
		@FillTime
		private Date createTime;

		@TableField(fill = FieldFill.UPDATE)
		@FillValue("111")
		private String color;
}

img_10.png 直接使用 UserLogConstantGen 里边的常量即可。 当出现没有自动生成 Gen 的情况,或者出现了gen包不存在的情况,请执行mvn clean compile试试,这种情况下可能是由于IDE没有自动重新编译代码导致的。

QueryHelper 查询助手

构建MyBatisPlus查询条件对象,提供以下静态方法:

java
 // 返回QueryWrapper对象,Q 可以为普通java bean,MPageQuery 子类,SortQuery 子类
 public static <Q, R> QueryWrapper<R> build(Q query);
  1. 参数query为MPageQuery子类为构建分页及排序QueryWrapper对象。
  2. 参数query为SortQuery子类为构建排序ueryWrapper对象。

注:query对象不带注解,构建QueryWrapper条件为等于,如果想使用非等于条件,需要在query参数上使用Query注解,并设置type属性,具体查看 org.zebra.mybatis.plus.core.query.QueryType。

其它

TypeHandler

提供三种 TypeHandler 的实现分别是:

  1. 整型列表以字符串存储 IntegerListTypeHandler
  2. 长整型列表以字符串存储 LongListTypeHandler
  3. 字符串列表以字符串存储 StringListTypeHandler