1、Mybatis-plus 简介
官网地址:https://mp.baomidou.com/guide/
MyBatis-Plus (简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
精髓-魂斗罗-搭档

特性
- 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
- 损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作
- 强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求
- 支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错
- 支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题
- 支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作
- 支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )
- 内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用
- 内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询
- 分页插件支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库
- 内置性能分析插件:可输出 Sql 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询
- 内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作
支持数据库
- mysql 、 mariadb 、 oracle 、 db2 、 h2 、 hsql 、 sqlite 、 postgresql 、 sqlserver
- 达梦数据库 、 虚谷数据库 、 人大金仓数据库
导入依赖中mysql5和mysq8的jdbc驱动区别需要注意
mysql5
spring.datasource.driver-class-name=com.mysql.sql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/mpdata?useSSL=false
spring.datasource.username=root
spring.datasource.password=123456
mysql8(Spring Boot2.1开始默认)
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/mpdata?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=123456
2、主键策略
MP有自己的默认对应规则,例如实体类名为:OwnUser,会给你对应到数据库中的own_user表。这是MP源代码中实现的实体与表的对应关系。如果你不想用默认的对应规则,可以使用@TableName()注解,进行表名指定。
关于id的一些生成策略,参考:https://www.cnblogs.com/haoxinyue/p/5208136.html
1、ID_WORKER
Mybatis-plus默认的主键策略是ID_WORKER (雪花算法)
snowflake(雪花算法)是Twitter开源的分布式ID生成算法,结果是一个long型的ID。其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0。具体实现的代码可以参看https://github.com/twitter/snowflake。
// 实体类 ID字段不加注解就是默认的ID_WORKER
@Data
public class User {
private Long id;
private String name;
private Integer age;
private String email;
@TableField(fill = FieldFill.INSERT)
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
// 引入乐观锁插件
@Version
private Integer version;
@TableLogic
private Integer delFlag;
}
通过设置@TableId可以让Mybatis-plus自动为我们生成雪花算法的ID号,该ID号是一个长整型数据,非常方便。但是雪花算法的ID号是在Insert执行的时候生成的,我们在Insert执行前是不知道Entity会获得一个什么ID号。如果想提前获取一个雪花id,可以调用IdWorker 类
package com.baomidou.mybatisplus.core.toolkit;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator;
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
/**
* id 获取器
*
* @author hubin
* @since 2016-08-01
*/
public class IdWorker {
/**
* 主机和进程的机器码
*/
private static IdentifierGenerator IDENTIFIER_GENERATOR = new DefaultIdentifierGenerator();
/**
* 毫秒格式化时间
*/
public static final DateTimeFormatter MILLISECOND = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS");
/**
* 获取唯一ID
*
* @return id
*/
public static long getId() {
return getId(new Object());
}
/**
* 获取唯一ID
*
* @return id
*/
public static long getId(Object entity) {
return IDENTIFIER_GENERATOR.nextId(entity).longValue();
}
/**
* 获取唯一ID
*
* @return id
*/
public static String getIdStr() {
return getIdStr(new Object());
}
/**
* 获取唯一ID
*
* @return id
*/
public static String getIdStr(Object entity) {
return IDENTIFIER_GENERATOR.nextId(entity).toString();
}
/**
* 格式化的毫秒时间
*/
public static String getMillisecond() {
return LocalDateTime.now().format(MILLISECOND);
}
/**
* 时间 ID = Time + ID
* <p>例如:可用于商品订单 ID</p>
*/
public static String getTimeId() {
return getMillisecond() + getIdStr();
}
/**
* 有参构造器
*
* @param workerId 工作机器 ID
* @param dataCenterId 序列号
* @see #setIdentifierGenerator(IdentifierGenerator)
*/
public static void initSequence(long workerId, long dataCenterId) {
IDENTIFIER_GENERATOR = new DefaultIdentifierGenerator(workerId, dataCenterId);
}
/**
* 自定义id 生成方式
* @param identifierGenerator id 生成器
* @see GlobalConfig#setIdentifierGenerator(IdentifierGenerator)
*/
public static void setIdentifierGenerator(IdentifierGenerator identifierGenerator) {
IDENTIFIER_GENERATOR = identifierGenerator;
}
/**
* 使用ThreadLocalRandom获取UUID获取更优的效果 去掉"-"
*/
public static String get32UUID() {
ThreadLocalRandom random = ThreadLocalRandom.current();
return new UUID(random.nextLong(), random.nextLong()).toString().replace(StringPool.DASH, StringPool.EMPTY);
}
}
2、自增策略
-
需要在创建数据表的时候设置主键自增
-
实体类ID字段配置@TableId(type = IdType.AUTO)
@Data public class User { //@TableId(type = IdType.AUTO) private Long id; private String name; private Integer age;
要想影响所有实体类的配置,可以在application.properties中设置全局主键配置
mybatis-plus.global-config.db-config.id-type=auto
3、其他主键策略,点击IdType源码得知
public enum IdType {
AUTO(0), // 数据库id自增
NONE(1), // 未设置主键类型
INPUT(2), // 用户手动setID,可以通过自己注册自动填充插件进行填充
// 以下3种只有ID为空,才自动填充
ID_WORKER(3),
UUID(4),
ID_WORKER_STR(5); // id_worker的字符串表示
private final int key;
private IdType(int key) {
this.key = key;
}
public int getKey() {
return this.key;
}
}
3、自动填充
阿里的规范中说了,数据库表表必须有三个字段:ID、创建时间、更新时间,自动填充时间有两种方式实现。

-
实体类上添加注解
@TableField(fill = FieldFill.INSERT) private Date createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private Date updateTime; -
新建一个handler包,实现元对象处理接口
/** * 处理元数据 * 1.实现 MetaObjectHandler * 2.添加到IOC容器中 */ @Slf4j @Component public class MybatisObjectHandler implements MetaObjectHandler { /** * 插入时候自动填充 * @param metaObject */ @Override public void insertFill(MetaObject metaObject) { log.info("start insert fill"); //三个参数: //实体类属性名 //要填充的内容,必须和实体类中字段类型相同 //metaObject //System.out.println("MybatisObjectHandler =>" +metaObject); this.setFieldValByName("createTime",new Date(),metaObject); this.setFieldValByName("updateTime",new Date(),metaObject); } // 更新时自动填充 @Override public void updateFill(MetaObject metaObject) { log.info("start update fill"); this.setFieldValByName("updateTime",new Date(),metaObject); } } -
测试

4、乐观锁
面试中经常会问乐观锁
悲观锁:无论干啥,都先给你锁上,再做操作,比如传统的synchronized,juc的ReentrantLock就是悲观锁。
乐观锁:非常乐观,无论什么操作都不加锁,当出现问题时再找解决方案
当更新一条记录,希望没有被别人更新,通常的方式就增加一个乐观锁字段(version)即可,更新的时候把version带上,如果version不对就更新失败。
-
添加@version注解到字段上
@Data public class User { ... @Version private Integer version; } -
创建配置类MybatisPlusConfig ,注入bean乐观锁插件
// 千万不要忘记开启事务管理
@EnableTransactionManagement
@Configuration
public class MybatisPlusConfig {
// 乐观锁插件
@Bean
public OptimisticLockerInterceptor optimisticLockerInterceptor() {
return new OptimisticLockerInterceptor();
}
}
-
测试乐观锁
// 测试乐观锁,模拟多线程抢占的情况下是什么样子的 @Test public void testOptimisticLokerFail(){ // 查询一个数据,返回当前的一个version User user = userMapper.selectById(1L); // 修改数据 user.setName("Jude1111"); user.setAge(30); // 插入第三者 User user2 = userMapper.selectById(1L); user2.setName("Jude2222"); user2.setAge(32); // 第三者抢先执行 userMapper.updateById(user2); // 自己再慢慢更新 if(userMapper.updateById(user) == 1){ System.out.println("update successfully"); }else { System.out.println("update fail due to modified by others"); } }
特别说明:
- 支持的数据类型只有:int,Integer,long,Long,Date,Timestamp,LocalDateTime
- 整数类型下
newVersion = oldVersion + 1newVersion会回写到entity中- 仅支持
updateById(id)与update(entity, wrapper)方法- 在
update(entity, wrapper)方法下,wrapper不能复用!!!
5、逻辑删除
逻辑删除:并不是真的从数据库中删除,只是加了一个删除状态标记。
物理删除:直接从数据库中删除
使用MP实现逻辑删除:
1.添加@TableLogic注解到deleted字段上
@Data
public class User {
@TableLogic
private Integer deleted;
}
2.application.properties加入配置,如果配置值与mp默认的一样,可以不用配
mybatis-plus.global-config.db-config.logic-delete-value=1
mybatis-plus.global-config.db-config.logic-not-delete-value=0
3.MybatisPlusConfig 中注册逻辑删除插件
// 千万不要忘记开启事务管理
@EnableTransactionManagement
@Configuration
public class MybatisPlusConfig {
// 乐观锁插件
// 本质是一个拦截器 Interceptor
@Bean
public OptimisticLockerInterceptor optimisticLockerInterceptor() {
return new OptimisticLockerInterceptor();
}
// 逻辑删除插件!
@Bean
public ISqlInjector sqlInjector() {
return new LogicSqlInjector();
}
}
4.测试逻辑删除
// 逻辑删除
@Test
public void testDelete(){
int i =userMapper.deleteById(2L);
System.out.println(i);
}
测试后发现deleted字段的值由0变成了1,且MP中的查询操作都会自动添加deleted字段的判断
@Test
public void testselect(){
List<User> userList = userMapper.selectList(null);
}
slelect id,name,age....from user where deleted = 0
小结, 按照自动填充、逻辑删除,乐观锁的规范,以后业务表至少包含5个字段:id,create_time,update_time,deleted,version
6、分页查询
MP自带分页插件,只要简单的配置即可实现分页功能,支持多种数据库:Mysql,MariaDB,Oracle,DB2,H2,SQLServer等。
-
MybatisPlusConfig 中注册分页插件
// 千万不要忘记开启事务管理 @EnableTransactionManagement @Configuration public class MybatisPlusConfig { // 乐观锁插件 // 本质是一个拦截器 Interceptor @Bean public OptimisticLockerInterceptor optimisticLockerInterceptor() { return new OptimisticLockerInterceptor(); } // 逻辑删除插件! @Bean public ISqlInjector sqlInjector() { return new LogicSqlInjector(); } // 分页插件 @Bean public PaginationInterceptor paginationInterceptor() { PaginationInterceptor paginationInterceptor = new PaginationInterceptor(); // 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认false // paginationInterceptor.setOverflow(false); // 设置最大单页限制数量,默认 500 条,-1 不受限制 // paginationInterceptor.setLimit(500); return paginationInterceptor; } } -
测试分页
// 分页实现 limit(sql) PageHelper Mp内置分页插件(导入即可!) @Test void testPage(){ // 当前页、总页数 //1、先查询总数 //2、本质还是 LIMIT 0,10 (默认的) // 参数 (当前页,每个页面的大小!) // 以后做分页就使用Page对象即可! Page<User> page = new Page<>(2,5); userMapper.selectPage(page,null); page.getRecords().forEach(System.out::println); // 列表数据 System.out.println(page.getSize()); // 每页记录数 System.out.println(page.getTotal()); // 总记录数 System.out.println(page.getCurrent()); // 当前页 System.out.println(page.hasNext()); // 是否有下一页 System.out.println(page.hasPrevious()); // 是否有上一页 }
7、性能分析插件
在开发时候排查慢SQL,MybatisPlusConfig注册性能分析插件即可
/**
* SQL执行效率插件
* 3.2.0以上版本已移除,推荐使用执行sql分析打印功能
*/
@Bean
@Profile({"dev","test"})// 设置 dev test 环境开启
public PerformanceInterceptor performanceInterceptor() {
PerformanceInterceptor per = new PerformanceInterceptor();
per.setMaxTime(1000); // 毫秒,超过此处设置的毫秒则sql不执行
per.setFormat(true); // 格式化sql
return per;
}
3.2.0以上版本已移除,推荐使用执行sql分析打印功能,可以参考官网教程使用

测试:
将最长时间设置为1ms
// 测试性能分析插件
@Test
public void testPerformance(){
User user = new User();
user.setName("coding");
user.setEmail("24736743@qq.com");
user.setAge(18);
userMapper.insert(user);
}
sql执行时间

抛出异常:The SQL execution time is too large
8、条件构造器
参考官方文档多使用就可以了
https://mp.baomidou.com/guide/wrapper.html#abstractwrapper
QueryWrapper
@Service
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService {
@Autowired
ProductMapper productMapper;
@Override
public void queryPage(Page<Product> pageParam,ProductQuery productQuery) {
if(productQuery == null){
this.page(pageParam,null);
return;
}
this.page(pageParam,new QueryWrapper<Product>() .like(!StringUtils.isEmpty(productQuery.getTitle()),"product_title",productQuery.getTitle()) .eq(!StringUtils.isEmpty(productQuery.getStatus()),"status",productQuery.getStatus())
}
}
UpdateWrapper
@RequestMapping("/updateStatus")
public R updateStatus(@RequestBody Map<String, Object> params) {
Object id = params.get("id");
String status = (String) params.get("status");
GoodsEntity goods = new GoodsEntity();
goods.setStatus(Integer.parseInt(status));
goodsService.update(goods,new UpdateWrapper<GoodsEntity>().eq("id",id));
return R.ok();
}
嵌套or,and
@Test
public void testUpdate(){
User user = new User();
user.setAge(22);
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
updateWrapper.ge("age",20)
.and(wrapper -> wrapper.eq("name","jude").or().eq("name","jack"));
userMapper.update(user,updateWrapper);
}
控制台打印SQL:
Time:79 ms - ID:com.icoding.mapper.UserMapper.update
Execute SQL:
UPDATE
user
SET
age=22,
update_time='2020-04-07 13:22:03.861'
WHERE
del_flag=0
AND age >= 20
AND (
name = 'jude'
OR name = 'jack'
)
注意:
插入或更新的字段有 空字符串 或者 null需求时的解决方法,都在官方文档https://mp.baomidou.com/guide/faq.html

9、baseMapper的方法注入
我们在用的时候经常就是生产自定义的Mapper继承自BaseMapper,然后我们就可以使用了,但是有没想过BaseMapper里的方法是怎么被注入到mybatis里的,怎样注入到mapper.xml文件的

MybatisPlusAutoConfiguration的SqlSessionFactory,创建bean

看applyConfiguration方法

MybatisConfiguration是继承自Configuration的,自定义了一个MybatisMapperRegistry注册器,后面会用到

MybatisSqlSessionFactoryBean的初始化后方法afterPropertiesSet调用buildSqlSessionFactory创建SqlSessionFactory:

点击buildSqlSessionFactory方法,里面有一段代码是会循环解析自定义mapper的xml文件的

点击parse方法

点击bindMapperForNamespace方法


其中有个addMapper的方法,是前面MybatisConfiguration调用的,点击它的addMapper方法,内部是调用MybatisMapperRegistry的方法:
/**
* 使用自己的 MybatisMapperRegistry
*/
@Override
public <T> void addMapper(Class<T> type) {
mybatisMapperRegistry.addMapper(type);
}
点击 mybatisMapperRegistry.addMapper(type)方法

里面会进行基本的sql方法注入

完成每个方法的注入:

注入的实现:

其实每一个AbstractMethod的子类都会实现自己的injectMappedStatement:

如BaseMapper的Insert方法

DeleteByMap方法

最后会去枚举类SqlMethod中获取对应的枚举,里面就是类似定义在xml中的信息,最后转换为sqlSource再进行封装:
String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), columnScript, valuesScript);
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
return this.addInsertMappedStatement(mapperClass, modelClass, getMethod(sqlMethod), sqlSource, keyGenerator, keyProperty, keyColumn);
SqlMethod 枚举值:

最终还是调用了MapperBuilderAssistant的addMappedStatement进行注册:
如上面的DeleteByMap,注入完sql语句后的返回
return addUpdateMappedStatement(mapperClass, modelClass, getMethod(sqlMethod), sqlSource);
或者
return this.addDeleteMappedStatement(mapperClass, getMethod(sqlMethod), sqlSource);
会看抽象类AbstractMethod的源码

都调用了addMappedStatement方法

总结
- 初始化注入自定义的MybatisConfiguration和MybatisMapperRegistry。
- 解析Mapper类,获取方法对应的AbstractMethod。
- 调用各自的实现进行去SqlMethod获取对应的枚举,获取到信息后进行注册。
其实就相当于代码里面定义了原本需要再xx.xml定义的数据,直接在代码中获取注入常用的CRUD操作即可
参考:https://blog.csdn.net/wangwei19871103/article/details/109771510