介绍
模块基于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 |
快速开始
引入
<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 示例:
@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。
- 继承org.zebra.mybatis.plus.core.MapperPlus,MapperPlus 里所有的接口都可以使用。
- 编写实体后,不编写 Mapper 类的话,会根据实体自动生成 Mapper,这就是所谓的动态Mapper,动态 Mapper 也继承了 MapperPlus 接口,里边所有接口都可以使用。
MapperPlus 增强了Sql处理接口分别为: 只描述动态生成Mapper如何使用,第一种使用为正常使用Mapper方法。
- 首先创建数据库表
CREATE TABLE "public"."user_test1"
(
"id" int4,
"name" varchar(255);
)
- 配置springboot 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}
- 创建实体类
@Data
@TableName("public.user_test1")
public class UserLog {
private Integer id;
private String name;
}
- 创建DTO
@Data
public class UserDto {
private Integer id;
private String name;
}
/**
* 返回多条结果
*
* @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());
}
/**
* 返回单条结果
*
* @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。
@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 抽象类分别为:
- ServicePlusImpl
- DynamicServicePlusImpl
一般情况下自定义的 Service 需要实现 ServicePlusImpl 或 DynamicServicePlusImpl 来使用 mybatisplus,stream-query,join 提供的接口。 如果是动态 Mapper 需要继承 DynamicServicePlusImpl,自定义 Mapper 需要继承 ServicePlusImpl,这两个类提供的所有接口都是一致的,使用方式也具有一致性。 使用方式如下:
/**
* 动态 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接口使用,参数为匿名函数
@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. 一对多查询 相关类如下:
类 | 字段 |
---|---|
实体类 UserEntity | Integer id<br/>String name |
实体类 AddressEntity | Integer id<br/>Integer userId<br/>String address |
dto类 UserDTO | Integer id<br/>String name<br/>List< AddressDTO > addressList |
dto类 AddressDTO | Integer id<br/>Integer userId<br/>String addrss |
全部映射,把数据库实体类AddressDO所有属性全部映射到 UserDTO 的 addressList 列表中 .selectCollection(AddressEntity.class, UserDTO::getAddressList)
@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);
}
}
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)
2. 一对一查询
类 | 字段 |
---|---|
实体类 UserEntity | Integer id<br/>String name |
实体类 AddressEntity | Integer id<br/>Integer userId<br/>String address |
dto类 UserDTO | Integer id<br/>String name<br/> AddressDTO address |
dto类 AddressDTO | Integer id<br/>Integer userId<br/>String address |
@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);
}
}
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)
3. 联表查询返回简单实体<>
类 | 字段 |
---|---|
实体类 UserEntity | Integer id<br/>String name |
实体类 AddressEntity | Integer id<br/>Integer userId<br/>String address |
dto类 UserDTO | Integer id<br/>String name<br/> String address <br/> Integer addressId |
@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);
}
}
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)
相关文档 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项目规范
@RestController
@RequestMapping("/iam/baseClasses")
@Api(tags = "班组班别接口")
public class BaseClassesController extends CrudController<BaseClassesServiceImpl,BaseClassesQuery,BaseClassesQuery,BaseClassesPageQuery,BaseClassesQuery> {
}
以下为默认提供的接口
描述 | 方法 | 类型 | url | 参数 |
---|---|---|---|---|
增加数据 | POST | json | http://localhost:9090/basic/user | 实体参数 |
修改数据 | Put | json | http://localhost:9090/basic/user | 实体参数 |
查询单条数据 | GET | url | http://localhost:9090/basic/user/id | id |
查询所有数据 | GET | url | http://localhost:9090/basic/user/list | 无参数查询所有 有实体参数为等于的条件查询 |
根据id删除数据 | DELETE | url | http://localhost:9090/basic/user/id | id |
根据多个id删除数据 | DELETE | url | http://localhost:9090/basic/user?ids | ids 多个用,分割 |
分页查询 | GET | url | http://localhost:9090/basic/user/page | 每页记录数size 当前页page 排序字段sort 排序 order desc,asc 详见 MPageQuery 使用 |
如果需要再默认接口上增加操作,如日志记录或者权限控制等,需要实现以下两个方法,方法执行前before,方法执行后after,通过method.getName()能够区分执行的哪个方法,进行区别化处理。
@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。
@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 忽略表或者列不进行审计记录,多个表用分号分隔规则如下:
- talbe1.column1,column2 : 表示忽略这个表的这2个字段
- table2.*: 表示忽略这张表的INSERT/UPDATE,delete暂时还保留
zebra:
database:
audit:
enabled: false
ignored-columns: table1.c1,c2;table2.*
- 字段名称如果要显示中文需要在实体上增加@ApiModelProperty注解 示例:
@Data
@TableName("public.user_test1")
public class UserLog {
@ApiModelProperty("id1")
private Integer id;
@ApiModelProperty("名称")
private String name;
@ApiModelProperty("年龄")
private String userAge;
}
- 如果想要更友好的显示某些值的含义,如数据库字段sex 1代表男,2代表女,需要实现以下接口,并注入到spring容器中
public interface DataAuditFieldTranslation {
/**
* 数据翻译
*
* @param tableName 表名
* @param columnName 字段名称
* @param columnValue 字段值
* @return
*/
public String translate(String tableName, String columnName, String columnValue);
}
示例:
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 发送出来,具体示例如下:
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 方法有很多种,下边只是其中一种
@Configuration
public class TestConfig {
public TestConfig(MetaObjectHandlerPlus metaObjectHandlerPlus) {
metaObjectHandlerPlus.addMetaObjectHandler(null);
}
}
注解介绍 @FillTime 自动赋值数据操作时间。需结合mybatis-plus原框架注解@TableField (该注解的使用请查看官方文档)一并使用才有效。 被标注的字段,在可允许的类型范围(String、Long、long、Date、LocalDate、LocalDateTime)内,数据被操作的情况下,会自动被赋值上当前时间。 如果使用String的话需要同时指明format参,用以确认格式化后的样式
属性 | 类型 | 必需 | 默认值 | 描述 |
---|---|---|---|---|
format | String | 非必需 | yyyy-MM-dd HH:mm:ss | 如果字段类型为String,需要制定字符串格式 |
@FillData 指定实现方式,自动赋值数据操信息。需结合mybatis-plus原框架注解@TableField (该注解的使用请查看官方文档)一并使用才有效。 被标注的字段,会根据@FillData中AuditHandler的实现来返回对应的值。 通常的实现方案都是用户信息(id、name等)放入header中,全局定义函数来获取
属性 | 类型 | 必需 | 默认值 | 描述 |
---|---|---|---|---|
value | Class> | 必需 | yyyy-MM-dd HH:mm:ss | 自定义用户生成方式 |
@FillValue 数据插入的时候字段的默认值,支持类型:String, Integer, int, Long, long, Boolean, boolean, Double, double, Float, float, BigDecimal, Date, LocalDate, LocalDateTime, 枚举(仅支持枚举的名字作为默认值)
属性 | 类型 | 必需 | 默认值 | 描述 |
---|---|---|---|---|
value | String | 必需 | 默认值 | |
format | boolean | 非必需 | yyyy-MM-dd HH:mm:ss | 如果字段类型为时间类型(Date,LocalDateTime等),需要制定字符串格式 |
示例:
@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 功能模块
属性 | 类型 | 是否必须 | 默认值 | 描述 |
---|---|---|---|---|
source | Class<?> | 是 | Void.class | 数据来源的Entity class |
field | String | 是 | 数据来源的Entity对应的属性 | |
conditions | Condition[] | 是 | 被关联的Entity所需要的条件 | |
updateCondition | Condition[] | 否 | 空 | 通常指被更新表自身的特殊条件,例如:enable=1 and is_deleted=0 |
@Condition 冗余字段的关联条件
属性 | 类型 | 是否必须 | 默认值 | 描述 |
---|---|---|---|---|
selfField | String | 是 | 关联数据来源Entity所需的自身字段 | |
sourceField | String | 是 | "id" | 数据来源的Entity的字段,默认为id |
示例: 假设地址表上需要冗余用户名名称,如果用户的名字有改动,则需要同步新的改动,代码如下:
@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 条件。 示例:
@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 接口为数据权限规则接口,分别有两个方法:
- Expression getExpression(Method method) 获取自定义的条件,使用者需要实现此接口,参数为调用的 mapper方法,返回的是 jsqlparser 开源库提供的条件对象,expression 其实转换成字符串格式就是 dept_id = 1 或 user_id = 2 类似这种条件,有可能为多个。
- default Expression parse(String sql) throws Exception 可以根据字符串返回条件对象,使用者不需要实现此接口
需要注意的是实现此接口的类必须注解 @Component 交给 spring 容器进行管理,以下具体实现
@Component
public class TestDataPermissionRule2 implements DataPermissionRule {
@Override
public Expression getExpression(Method method) {
return null;
}
}
以上数据权限接口和注解只是一个模板,没有具体实现,以下实现了一个比较常见的基于角色的数据权限规则 通常情况下有以下5种类型
- 全部数据权限,表示拥有所有部门的数据权限 ·
- 自定数据权限,表示拥有指定的若干部门的数据权限
- 本部门数据权限,表示仅拥有用户所属部门(不包括子部门)的数据权限
- 本部门及以下数据权限,表示仅拥有用户所属部门和所有子部门的数据权限
- 仅本人数据权限,表示仅拥有用户本人的数据权限
同时后台管理界面上也需要支持数据权限的操作如下:
使用 DeptDataPermissionField 注解此注解要和 DataPermission 注解联合使用,一起注解到 mapper 方法上,并提供以下属性:
属性 | 描述 |
---|---|
userIdFieldName | 数据库中,用户id字段,默认为 user_id |
userIdFieldType | 数据库中,用户id类型,默认为 FieldType.NUMBER |
deptIdFieldName | 数据库中,部门id字段,默认为 dept_id |
deptIdFieldType | 数据库中,部门id类型,默认为 FieldType.NUMBER |
编写类继承 DeptDataPermissionRule 抽象类,并实现以下方法:
/**
* 根据角色id获取所有部门id
*/
protected abstract Set<String> getDeptIdByCustom(String roleId);
/**
* 根据部门id获取当前和所有下级部门的id
*/
protected abstract Set<String> getDeptAndChildId(String deptId);
/**
* 获取当前用户权限部门信息
*
* @return
*/
protected abstract DataPermissionCurrentUser getDataPermissionCurrentUser();
DataPermissionCurrentUser 类结构如下,需要把值填充完成。
@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
- 部门数据权限 2
- 部门及以下数据权限 3
- 仅本人数据权限 4
- 自定数据权限 5
DeptDataPermissionRule 实现类示例:
@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有关系,如有表别名,就需要把表别名也加上
@DataPermission
@DeptDataPermissionField(userIdFieldName = "d.user_id", deptIdFieldName = "r.dept_id")
Page<SysRoleVo> selectPageRoleList(@Param("page") Page<SysRole> page, @Param(Constants.WRAPPER) Wrapper<SysRole> queryWrapper);
<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 示例
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 监控配置
- enabled 为是否启用 默认为true
- aop-patterns 主要用来监控SpringBean 默认为当前工程的包路径,可以不进行配置 示例 com.ninemax.example.*
- slow-sql-millis 慢sql记录 单位毫秒 可以不进行配置
- username web console 登录名 可以不进行配置则不需要登录名
- password web console 密码 可以不进行配置则不需要密码
- allow web console 访问控制,不进行配置则访问不进行限制,配置127.0.0.1只对本机访问开发
zebra:
database:
monitor:
enabled: true
aop-patterns:
slow-sql-millis:
username:
password:
allow:
web 管理控制台
字段常量
某个地方的代码想使用下领域字段的名称,但是又不想写死一个字符串(编译期不可校验)。 使用 @DomainConstantGen 注解 maven 编译后自动生成字段常量代码,生成策略为 在所在类的包下gen目录中并且以Gen为后缀。
@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;
}
直接使用 UserLogConstantGen 里边的常量即可。 当出现没有自动生成 Gen 的情况,或者出现了gen包不存在的情况,请执行mvn clean compile试试,这种情况下可能是由于IDE没有自动重新编译代码导致的。
QueryHelper 查询助手
构建MyBatisPlus查询条件对象,提供以下静态方法:
// 返回QueryWrapper对象,Q 可以为普通java bean,MPageQuery 子类,SortQuery 子类
public static <Q, R> QueryWrapper<R> build(Q query);
- 参数query为MPageQuery子类为构建分页及排序QueryWrapper对象。
- 参数query为SortQuery子类为构建排序ueryWrapper对象。
注:query对象不带注解,构建QueryWrapper条件为等于,如果想使用非等于条件,需要在query参数上使用Query注解,并设置type属性,具体查看 org.zebra.mybatis.plus.core.query.QueryType。
其它
TypeHandler
提供三种 TypeHandler 的实现分别是:
- 整型列表以字符串存储 IntegerListTypeHandler
- 长整型列表以字符串存储 LongListTypeHandler
- 字符串列表以字符串存储 StringListTypeHandler