之前做列表或者报表功能时使用最多的是pageHelper插件做Mybatis的分页查询,MP为了方便将分页做了一个相关的模块com.baomidou.mybatisplus.extension.plugins.pagination供我们分页查询时去使用.
8.1.1. 配置插件在spring的配置文件中,在sqlsessionFactory里面增加插件相关的配置,增加分页插件即可。
@Test
public void testLogicDeleteById() {
userMapper.deleteById(1);
userMapper.selectById(1);
//要把逻辑删除的数据查询出来再次改为正常呢?
List userList = userMapper.selectList(new LambdaQueryWrapper().eq(User::getIsDelete, 1));
//查询失败
userList.forEach(System.out::println);
}
输出的sql,可以看到真正执行的是更新操作,将is_delete设置为1;
14 16:17:55.343 [main] DEBUG com.zzlh.mp.mapper.UserMapper.deleteById - ==>
UPDATE user SET is_delete=1
WHERe id=1 AND is_delete=0;
------------------------------------------------------------------------------------------------------------------------
15 16:17:55.348 [main] DEBUG com.zzlh.mp.mapper.UserMapper.selectById - ==>
SELECT id,name,age,email,is_delete
FROM user
WHERe id=1 AND is_delete=0;
------------------------------------------------------------------------------------------------------------------------
SELECT id,name,age,email,is_delete
FROM user
WHERe is_delete=0 AND is_delete = 1;
------------------------------------------------------------------------------------------------------------------------
注:
- 使用mp自带方法删除和查找都会附带逻辑删除功能 (自己写的xml不会)
- 询问MP开发者后 开发者说明 因为大部分情况在删除之后不会恢复 所以没有设定相关恢复或者跳过 当前 筛选的 接口
解决了繁琐的配置,让 mybatis 优雅的使用枚举属性!
主要是升级后的改变:
自3.1.0开始,可配置默认枚举处理类来省略扫描通用枚举配置 默认枚举配置
-
升级说明:
3.1.0 以下版本改变了原生默认行为,升级时请将默认枚举设置为EnumOrdinalTypeHandler
-
影响用户:
实体中使用原生枚举
-
其他说明:
配置枚举包扫描的时候能提前注册使用注解枚举的缓存
-
推荐配置:
- 使用实现IEnum接口
- 推荐配置defaultEnumTypeHandler
- 使用注解枚举处理
- 推荐配置typeEnumsPackage
- 注解枚举处理与IEnum接口
- 推荐配置typeEnumsPackage
- 与原生枚举混用
- 需配置defaultEnumTypeHandler与 typeEnumsPackage
- 使用实现IEnum接口
方式一,枚举属性,实现 IEnum 接口
public enum AgeEnum implements IEnum {
ONE(1, "一岁"),
TWO(2, "二岁"),
THREE(3, "三岁");
private int value;
private String desc;
AgeEnum(final int value, final String desc) {
this.value = value;
this.desc = desc;
}
@Override
public Integer getValue() {
return value;
}
}
方式二,原生的枚举类型,官网现在已经没有这个实例了
public enum GenderEnum {
MALE,
FEMALE;
}
方式三,使用 @EnumValue 注解枚举属性
public enum GradeEnum {
PRIMARY(1, "小学"),
SECONDORY(2, "中学"),
HIGH(3, "高中");
GradeEnum(int code, String descp) {
this.code = code;
this.descp = descp;
}
@EnumValue//标记数据库存的值是code
private final int code;
private final String descp;
public int getCode() {
return code;
}
public String getDescp() {
return descp;
}
}
实体属性使用枚举类型,只展示使用枚举的属性。
public class User {
private AgeEnum age;
private GenderEnum gender;
private GradeEnum grade;
}
8.3.2. 配置扫描通用枚举
8.3.3. 编写测试代码
@Test
public void insert() {
User user = new User();
user.setName("K神");
user.setAge(AgeEnum.ONE);
user.setGrade(GradeEnum.HIGH);
user.setGender(GenderEnum.MALE);
user.setEmail("abc@mp.com");
System.out.println("result:"+userMapper.insert(user));
// 成功直接拿会写的 ID
System.err.println("n插入成功 ID 为:" + user.getId());
List list = userMapper.selectList(null);
for(User u:list){
System.out.println(u);
System.out.println(u.getAge());
if(u.getId().equals(user.getId())){
System.out.println(u.getGender());
System.out.println(u.getGrade());
}
}
}
@Test
public void delete() {
System.out.println("result:"+userMapper.delete(new QueryWrapper()
.lambda().eq(User::getAge, AgeEnum.TWO)));
}
@Test
public void update() {
System.out.println("result:"+userMapper.update(new User().setAge(AgeEnum.TWO),
new QueryWrapper().eq("age", AgeEnum.THREE)));
}
@Test
public void select() {
User user = userMapper.selectOne(new QueryWrapper().lambda().eq(User::getId, 2));
System.out.println("name:"+user.getName());
System.out.println("age:"+user.getAge());
}
对应的SQL和输出
insert
INSERT INTO user ( id, name, age, gender, grade, email ) VALUES ( 1152049395573956610, 'K神', 1, 0, 3, 'abc@mp.com' );
------------------------------------------------------------------------------------------------------------------------
插入成功 ID 为:1152049395573956610
SELECT id,name,age,gender,grade,email FROM user
------------------------------------------------------------------------------------------------------------------------
User(id=2, name=张三, age=null, gender=FEMALE, grade=SECONDORY, email=zs@mp.com)
null
User(id=1152049395573956610, name=K神, age=ONE, gender=MALE, grade=HIGH, email=abc@mp.com)
ONE
MALE
HIGH
delete
DELETE FROM user WHERe age = 2
------------------------------------------------------------------------------------------------------------------------
update
UPDATE user SET age=2 WHERe age = 3
------------------------------------------------------------------------------------------------------------------------
select
SELECT id,name,age,gender,grade,email FROM user WHERe id = 2;
name:张三
age:null
------------------------------------------------------------------------------------------------------------------------
8.3.4. JSON序列化处理
如果需要对使用了通用枚举的对象进行json序列化转化,比如restful风格的,可以使用JSON序列化处理来帮住枚举的转换。
官方提供了两种工具包的转换方案:
在需要响应描述字段的get方法上添加@JsonValue注解即可,官方文档上说还需要在枚举中复写toString方法经过试验这个包中不需要复写即可。
public enum GradeEnum {
PRIMARY(1, "小学"),
SECONDORY(2, "中学"),
HIGH(3, "高中");
GradeEnum(int code, String descp) {
this.code = code;
this.descp = descp;
}
@EnumValue
private final int code;
private final String descp;
public int getCode() {
return code;
}
@JsonValue
public String getDescp() {
return descp;
}
}
编写测试类
@Test
public void select() throws JsonProcessingException {
User user = userMapper.selectOne(new QueryWrapper().lambda().eq(User::getId, 2));
System.out.println(JSON.toJSONString(user));
//{"age":"ONE","email":"zs@mp.com","gender":"FEMALE","grade":"SECONDORY","id":2,"name":"张三"}
System.out.println(new ObjectMapper().writevalueAsString(user));
//{"id":2,"name":"张三","age":"一岁","gender":"FEMALE","grade":"中学","email":"zs@mp.com"}
}
8.3.4.2. Fastjson
Fastjson在实验中并没有实现,去找官方提供的实例中,并没有关于JSON序列化处理这一块的例子,只是演示了使用通用枚举属性相关的东西。然后我联系了官方的人员,希望能得到这一块的相关的使用说明,结果被回复了五个字自己去百度。
我自己找Fastjson相关资料的时候,看了注解中SerializerFeature.WriteEnumUsingToString这一点,这个是Fastjson提供的关于枚举类型序列化的时候处理方案的一种,我看了里面有好几种,那么我猜测这些序列化的内容应该是Fastjson包自己的东西而不是MP的特性?这个有待不使用MP的时候再验证。
我又去找官方网站上介绍的优秀案例上看看能不能找到相关的例子,只在Crown这个项目里找到使用枚举的情况,但是项目中使用的是Jackson这种方式。
悲观锁:认为每次对数据的操作的时候,数据都是有风险的、不安全的、随时随地都会被别人。所以当开始一个修改数据操作的时候。该条数据被锁定,直到本次修改完成之后,再释放锁。
乐观锁:当要更新一条记录的时候,希望这条记录没有被别人更新。
特别说明:
支持的数据类型只有:int,Integer,long,Long,Date,Timestamp,LocalDateTime
整数类型下 newVersion = oldVersion + 1,newVersion 会回写到 entity 中
仅支持 updateById(id) 与 update(entity, wrapper) 方法
在 update(entity, wrapper) 方法下, wrapper 不能复用!!!
乐观锁实现方式:
取出记录时,获取当前version
更新时,带上这个version
执行更新时, set version = newVersion where version = oldVersion
如果version不对,就更新失败
8.4.1. 配置乐观锁插件在spring的配置文件里,将乐观锁插件com.baomidou.mybatisplus.extension.plugins.OptimisticLockerInterceptor配置到sqlSessionFactory里面。
@Test
public void testUpdateByIdSucc() {
User user = new User();
user.setAge(18);
user.setEmail("test@baomidou.com");
user.setName("optlocker");
user.setVersion(1);
userMapper.insert(user);
Long id = user.getId();
User userUpdate = new User();
userUpdate.setId(id);
userUpdate.setAge(19);
userUpdate.setVersion(1);
Assert.assertEquals("Should update success", 1, userMapper.updateById(userUpdate));
Assert.assertEquals("Should version = version+1", 2, userUpdate.getVersion().intValue());
}
@Test
public void testUpdateByIdFail() {
User user = new User();
user.setAge(18);
user.setEmail("test@baomidou.com");
user.setName("optlocker");
user.setVersion(1);
userMapper.insert(user);
Long id = user.getId();
User userUpdate = new User();
userUpdate.setId(id);
userUpdate.setAge(19);
userUpdate.setVersion(0);
Assert.assertEquals("Should update failed due to incorrect version(actually 1, but 0 passed in)", 0, userMapper.updateById(userUpdate));
}
@Test
public void testUpdateByIdSuccWithNoVersion() {
User user = new User();
user.setAge(18);
user.setEmail("test@baomidou.com");
user.setName("optlocker");
user.setVersion(1);
userMapper.insert(user);
Long id = user.getId();
User userUpdate = new User();
userUpdate.setId(id);
userUpdate.setAge(19);
userUpdate.setVersion(null);
Assert.assertEquals("Should update success as no version passed in", 1, userMapper.updateById(userUpdate));
User updated = userMapper.selectById(id);
Assert.assertEquals("Version not changed", 1, updated.getVersion().intValue());
Assert.assertEquals("Age updated", 19, updated.getAge().intValue());
}
@Test
public void testUpdateByEntitySucc() {
QueryWrapper ew = new QueryWrapper<>();
ew.eq("version", 1);
int count = userMapper.selectCount(ew);
User entity = new User();
entity.setAge(28);
entity.setVersion(1);
Assert.assertEquals("updated records should be same", count, userMapper.update(entity, null));
ew = new QueryWrapper<>();
ew.eq("version", 1);
Assert.assertEquals("No records found with version=1", 0, userMapper.selectCount(ew).intValue());
ew = new QueryWrapper<>();
ew.eq("version", 2);
Assert.assertEquals("All records with version=1 should be updated to version=2", count, userMapper.selectCount(ew).intValue());
}
对应的SQL和输出
testUpdateByIdSucc
INSERT INTO user ( name, age, email, version ) VALUES ( 'optlocker', 18, 'test@baomidou.com', 1 );
------------------------------------------------------------------------------------------------------------------------
UPDATE user SET age=19, version=2
WHERe id=18 AND version=1;
------------------------------------------------------------------------------------------------------------------------
testUpdateByIdFail
INSERT INTO user ( name, age, email, version ) VALUES ( 'optlocker', 18, 'test@baomidou.com', 1 );
------------------------------------------------------------------------------------------------------------------------
UPDATE user SET age=19, version=3
WHERe id=19 AND version=2;
------------------------------------------------------------------------------------------------------------------------
testUpdateByIdSuccWithNoVersion
INSERT INTO user ( name, age, email, version ) VALUES ( 'optlocker', 18, 'test@baomidou.com', 1 );
------------------------------------------------------------------------------------------------------------------------
UPDATE user SET age=19
WHERe id=20;
------------------------------------------------------------------------------------------------------------------------
SELECT id,name,age,email,version
FROM user
WHERe id=20;
------------------------------------------------------------------------------------------------------------------------
testUpdateByEntitySucc
SELECT COUNT( 1 )
FROM user
WHERe version = 1;
------------------------------------------------------------------------------------------------------------------------
UPDATe user SET age=28, version=2
WHERe version = 1;
------------------------------------------------------------------------------------------------------------------------
SELECT COUNT( 1 )
FROM user
WHERe version = 1;
------------------------------------------------------------------------------------------------------------------------
SELECt COUNT( 1 )
FROM user
WHERe version = 2;
------------------------------------------------------------------------------------------------------------------------
8.5. 执行 SQL 分析打印
该功能依赖 p6spy 组件,完美的输出打印 SQL 及执行时长 3.1.0 以上版本。也就是说我们不需要使用idea的插件,就能在控制台输出sql语句。
8.5.1. 引入p6spy包
p6spy
p6spy
3.8.2
8.5.2. 配置p6spy连接设置
在spring配置文件里面,对原来的dataSource进行修改,外面包一层p6spy的配置。
然后在resources的根目录下,增加一个spy.properties配置文件,类似于log4j的控制台打印设置。
module.log=com.p6spy.engine.logging.P6LogFactory,com.p6spy.engine.outage.P6OutageFactory
# 自定义日志打印
logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger
#日志输出到控制台
appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger
# 使用日志系统记录 sql
#appender=com.p6spy.engine.spy.appender.Slf4JLogger
# 设置 p6spy driver 代理
deregisterdrivers=true
# 取消JDBC URL前缀
useprefix=true
# 配置记录 Log 例外,可去掉的结果集有error,info,batch,debug,statement,commit,rollback,result,resultset.
excludecategories=info,debug,result,batch,resultset
# 日期格式
dateformat=yyyy-MM-dd HH:mm:ss
# 实际驱动可多个
#driverlist=org.h2.Driver
# 是否开启慢SQL记录
outagedetection=true
# 慢SQL记录标准 2 秒
outagedetectioninterval=2
注:该插件有性能损耗,不建议生产环境使用。
8.5.3. 控制台打印输出控制台就会打印出真实的sql语句和执行时间了。
8.6. 性能分析插件跟执行SQL分析打印比较类似,性能分析拦截器,用于输出每条 SQL 语句及其执行时间。
8.6.1. 配置性能分析插件在spring配置文件的sqlSessionFactory里面配置插件,com.baomidou.mybatisplus.extension.plugins.PerformanceInterceptor。
可以看到改插件有两个属性,format是配置sql是否格式化的,而maxTime是配置sql执行最大时长,如果sql语句执行时长超过配置的时间就会报异常,有助于测试的时候排查sql语句的性能。
注:此插件只用于开发环境或测试环境,不建议生产环境使用。
这里的sql注入器和sql注入攻击没有关系,是指除了通用CRUD提供的17中方法之外,通过sql注入器将自定义的方法注入到MyBatis容器中,只要继承baseMapper就能使用的功能插件。
在讲解MP加载自动注入的时候,说过通用的CRUD操作是AbstractSqlInjector类的inspectInject方法来实现注入的。AbstractSqlInjector是实现了ISqlInjector 接口,而自定义的通用方法类实现ISqlInjector的inspectInject方法,然后配置到spring里面,则也可以实现将自定义的方法注入到Mybatis的容器中。
继承AbstractMethod类,复写injectMappedStatement方法,使用addDeleteMappedStatement将sqlSource和method生成一个新的MappedStatement。
public class DeleteAll extends AbstractMethod {
@Override
public MappedStatement injectMappedStatement(Class> mapperClass, Class> modelClass, TableInfo tableInfo) {
String sql = "delete from " + tableInfo.getTableName();
String method = "deleteAll";
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
return this.addDeleteMappedStatement(mapperClass, method, sqlSource);
}
}
8.7.2. 编写自定义Sql注入器
继承DefaultSqlInjector,复写getMethodList方法。将自定的通用方法加入到方法列表中,也就是说除了默认的17个方法之外,增加一个新的自定义方法。
public class MySqlInjector extends DefaultSqlInjector {
@Override
public List getMethodList(Class> mapperClass) {
List methodList = super.getMethodList(mapperClass);
//增加自定义方法
methodList.add(new DeleteAll());
return methodList;
}
}
8.7.3. 将自定义的SQL注入器配置到spring里
8.7.4. 编写测试代码
Mapper.java文件继承baseMapper之后,将自定义的方法写好,不用写SQL语句将会自动注入。
public interface StudentMapper extends baseMapper {
void deleteAll();
}
测试类里面就可以直接使用deleteAll方法。
@Autowired
private UserMapper userMapper;
@Test
public void test(){
userMapper.deleteAll();
}
可以看一下控制台的输出,是不是使用了我们自定义的sql语句。
Time:19 ms - ID:com.zzlh.mp.mapper.UserMapper.deleteAll
Execute SQL:
delete
from
USER
8.8. 自动填充功能
这个插件是在基础使用场景中,最后一个插件,也是很重要的、使用很频繁的插件。
自动填充功能类似于数据库字段的默认值,也就是说当开启了这个功能,并指定字段使用自动填充功能,在没有给实例的字段设置值的时候,就会自动把设置好的值填充到实例的字段里,最终在生成的SQL语句里也会拼接上设置值,持久化到数据库。但是它比数据库的默认值优势在于,第一、可以指定场景,是插入的时候设置默认值还是更新的时候设置默认值,或者两种情况都设置。第二、自动填充的也可以是通过方法计算的可变化的值。
那么,根据该功能的情况,我们结合之前学习到的主键策略类型中的INPUT类型,将策略配置为INPUT,然后配置主键字段自动填充,从而使用我们自定义的主键生成规则生成主键。或者如果有这样的需求,每个数据表中都有创建时间和最后更新时间的字段用来标记数据的时间,那么通过配置插入和更新的自动填充,就可以实现该业务需求了。
8.8.1. 编写自定义的填充器这里我把内控中使用的UUIDUtils移植过来,来填充ID。
public class MymetaObjectHandler implements metaObjectHandler {
@Override
public void insertFill(metaObject metaObject) {
System.out.println("start insert fill ....");
//避免使用metaObject.setValue()
this.setFieldValByName("id", UUIDUtils.getUUID(), metaObject);
this.setFieldValByName("createtime", new Date(), metaObject);
this.setFieldValByName("modifytime", new Date(), metaObject);
}
@Override
public void updateFill(metaObject metaObject) {
System.out.println("start update fill ....");
this.setFieldValByName("modifytime", new Date(), metaObject);
}
}
注:
- 必须使用父类的setFieldValByName()或者setInsertFieldValByName/setUpdateFieldValByName方法,否则不会根据注解FieldFill.xxx来区分。
- 填充属性的字段不再是数据表中的字段名,而是实体中的属性名。因为自动填充是自动将值填充到了实例中,再由MP转换为SQL语句。
自动注入器需要配置到全局配置中,如图所示。
8.8.3. 编写测试代码
@Test
public void test(){
//先新增一条数据,ID为null,不设置创建时间和更新时间
User user = new User(null,"Tom",1,"tom@qq.com",null);
userMapper.insert(user);
//将新增的数据查询出来
User updateUser = userMapper.selectById(user.getId());
//不做任何修改更新数据
userMapper.updateById(updateUser);
}
控制台输出的SQL语句为:
INSERT INTO USER ( ID, NAME, AGE, EMAIL, OPERATOR, create_time, modify_time ) VALUES ( '201907251004021911432201111', '张三', 20, '22@qq.com', '管理员', '2019-07-25 10:04:02.191', '2019-07-25 10:04:02.191' );
------------------------------------------------------------------------------------------------------------------------
SELECT ID,NAME,AGE,EMAIL,OPERATOR,create_time,modify_time
FROM USER
WHERe ID='201907251004021911432201111';
------------------------------------------------------------------------------------------------------------------------
UPDATE USER SET NAME='张三', AGE=20, EMAIL='22@qq.com', OPERATOR='管理员', create_time='2019-07-25 10:04:02.0', modify_time='2019-07-25 10:04:50.718'
WHERe ID='201907251004021911432201111';
------------------------------------------------------------------------------------------------------------------------
8.9. 动态数据源
8.10. 分布式事务
8.11. 多租户 SQL 解析器
需要分页拦截器 使用,主要过滤查询数据。
8.12. 动态表名 SQL 解析器


