一、🎄MybatisPlus概述
为什么要学习它呢 ? MyBatisplus可以节省我们大量工作时间,所有的CRUD代码它都可以自动化完成!
简介
官网:MyBatis-Plus
MyBatis-Plus (opens new window)(简称 MP)是一个 MyBatis (opens new window)的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
愿景
我们的愿景是成为 MyBatis 最好的搭档,就像 魂斗罗 中的 1P、2P,基友搭配,效率翻倍。
特性:
- 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
- 损耗小:启动即会自动注入基本 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,Oracle,DB2,H2,HSQL,SQLite,PostgreSQL,SQLServer,Phoenix,Gauss ,ClickHouse,Sybase,OceanBase,Firebird,Cubrid,Goldilocks,csiidb,informix,TDengine,redshift
- 达梦数据库,虚谷数据库,人大金仓数据库,南大通用(华库)数据库,南大通用数据库,神通数据库,瀚高数据库,优炫数据库,星瑞格数据库
框架结构
二、🎏快速使用
官方文档-快速开始 | MyBatis-Plus (baomidou.com)
本文使用的Mybatis-Plus版本为最新的3.5.3.2,SpringBoot版本为2.7.15
技能掌握前提:
- 拥有 Java 开发环境以及相应 IDE
- 熟悉 Spring Boot
- 熟悉 Maven
步骤
-
创建数据库mybatis_plus
-
创建user表
DROP TABLE IF EXISTS user; CREATE TABLE user ( id BIGINT(20) NOT NULL COMMENT '主键ID', name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名', age INT(11) NULL DEFAULT NULL COMMENT '年龄', email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱', PRIMARY KEY (id) ); --真实开发中version(乐观锁),deleted(逻辑删除),gmt_create,gmt_modif都是必要的字段,后面会添加上。 --插入相关数据 DELETE FROM user; INSERT INTO user (id, name, age, email) VALUES (1, 'Jone', 18, 'test1@baomidou.com'), (2, 'Jack', 20, 'test2@baomidou.com'), (3, 'Tom', 28, 'test3@baomidou.com'), (4, 'Sandy', 21, 'test4@baomidou.com'), (5, 'Billie', 24, 'test5@baomidou.com'); -
使用SpringBoot初始化项目
-
添加依赖
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.28</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.22</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.2</version> </dependency>提示:不要同时导入mybatis和mybatis-plus -
连接数据库
注意:Mysql5和Mysql8的配置不同,mysql8的url配置需要添加时区,这儿使用的是yaml格式进行配置,后面所有配置都将使用yamlspring: datasource: url: jdbc:mysql://localhost:3306/mybatis_plus?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&allowMultiQueries=true username: root password: toor driver-class-name: com.mysql.cj.jdbc.Driver -
==传统方式:pojo-dao ( 连接mybatis,配置mapper.xml文件 )-service-controller==
-
现在:
-
创建pojo
package com.mybatisplus.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor //注意,此处开启了链式编程 @Accessors(chain = true) public class User { // 对应教据库中的主键(uuid、自id、雪化算、redis、zookeeper) private Long id; private String name; private Integer age; private String email; } -
创建mapper接口
package com.mybatisplus.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.mybatisplus.pojo.User; import org.springframework.stereotype.Repository; @Repository public interface UserMapper extends BaseMapper<User> { //所有CRUD操作以及完成,不需要写SQL } -
使用
@SpringBootTest class MybatisPlusApplicationTests { //继承自BaseMapper,所有方法都来自父类,也可以编写自己的扩展方法 @Resource private UserMapper userMapper; @Test void contextLoads() { //通过id查询用户 System.out.println(userMapper.selectById(1)); //查询全部用户,其参数是一个wrapper条件构造器,不需要则使用null userMapper.selectList(null).forEach(System.out::println); } }注意:需要在主启动类上使用MapperScan注解扫描所有mapper -
运行效果
CRUD的方法和SQL都由Mybatis-Plus帮我们编写完成
-
三、🎁配置日志
官方文档-日志配置 | MyBatis-Plus (baomidou.com)
配置日志的原因
我们现在所有的执行的SQL是不可见的,我们想要在开发过程中进行调试时,必须查看日志
#日志配置
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
效果:所有执行的SQL都被输出
四、🎪CRUD扩展
插入操作
@Test
void testInsert() {
User user = new User().setName("张三").setAge(12).setEmail("123@qq.com");
int result = userMapper.insert(user);
System.out.println(insert);
}
效果:
数据库插入的id的默认值为:全局的唯一id
主键生成策略
官方文档-主键策略 | MyBatis-Plus (baomidou.com)
User类中的id对应数据库中的主键,其中常见数据库主键自增策略有,uuid、自增id、雪化算法,在这儿用到的自增策略便是==雪花算法==
雪花算法(SnowFlake)简介
该算法由Twitter公司发明,主要目的是==解决在分布式环境下ID的生成问题==。雪花算法生成的ID是一个64位的整数,由时间戳、机器标识和序列号组成。时间戳部分占据41位,可以使用69年;机器标识部分占据10位,最多可以在1024个节点(包括5位datacenterId和5位workerId)中部署;序列号部分占据12位,同一毫秒时间戳可以生成4096个不重复的ID。因此,雪花算法能够保证生成的ID是==唯一==且自增的。
datacenterId的意思为数据中心的id,比如有些服务器在北京,香港,伦敦
workerId的意思为机器id,标识生成ID的机器的10位数字
主键默认生成策略
mybatis-plus默认主键自增策略
自 3.3.0 开始,默认使用雪花算法+UUID(不含中划线)
在User类中的id字段上添加注解==@TableId(type = IdType.ASSIGN_ID)==。
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public class User {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String name;
private Integer age;
private String email;
}
@Test
void testInsert() {
User user = new User().setName("张三").setAge(12).setEmail("123@qq.com");
int insert = userMapper.insert(user);
System.out.println(insert);
}
其它自增主键策略
主键自增
配置主键自增:
-
实体类上id字段加上注解==@TableId(type = IdType.AUTO)==
-
数据库字段也要设置自增长
-
实现效果
其余自增效果的源码解释
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.baomidou.mybatisplus.annotation;
public enum IdType {
AUTO(0),//自增
NONE(1),//不使用
INPUT(2),//手动输入
ASSIGN_ID(3),//分配id
ASSIGN_UUID(4);//分配uuid
private final int key;
private IdType(int key) {
this.key = key;
}
public int getKey() {
return this.key;
}
}
实体类上id字段加上注解==@TableId(type = IdType.INPUT)==
执行插入测试
@Test
void testInsert() {
User user = new User()
.setName("张三")
.setAge(12)
.setEmail("123@qq.com");
userMapper.insert(user);
}
更新操作
@Test
void testUpdate() {
User user = new User()
//通过条件自动拼接动态SQL
.setId(1L)
.setName("王五")
.setAge(8)
.setEmail("456@gmail.com");
//注意updateById,其传入的参数是一个对象
int update = userMapper.updateById(user);
System.out.println(update);
}
所有SQL都是自动配置
自动填充
官方文档-自动填充 | MyBatis-Plus (baomidou.com)
创建时间,修改时间,这些操作都是自动化完成,我们不希望手动更新
阿里巴巴开发手册:必备的三个字段id, gmt_create, gmt_modified。
方式一:数据库级别(不建议)
-
在数据库中新增字段,create_time和update_time
-
再次测试插入方法,需要现在实体类同步
@Data @AllArgsConstructor @NoArgsConstructor @Accessors(chain = true) public class User { // 对应教据库中的主键(uuid、自id、雪化算、redis、zookeeper) @TableId(type = IdType.ASSIGN_ID) private Long id; private String name; private Integer age; private String email; //新增的字段 private Date createTime; private Date updateTime; }@Test void testInsert() { User user = new User() .setName("张三") .setAge(12) .setEmail("123@qq.com"); int insert = userMapper.insert(user); System.out.println(insert); } -
再次更新
@Test void testUpdate() { User user = new User() .setId(1698244309027500034L) .setName("王五") .setAge(8) .setEmail("456@gmail.com"); int update = userMapper.updateById(user); System.out.println(update); } }
方式二:代码级别
-
去掉所有数据库中create_time和update_time字段的默认值
-
在实体类上的相关字段上添加注解@TableField
//字段添加填充内容 @TableField(fill = FieldFill.INSERT) private Date createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private Date updateTime; -
编写处理器来处理这个注解
package com.mybatisplus.handler; import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.reflection.MetaObject; import org.springframework.stereotype.Component; import java.time.LocalDateTime; import java.util.Date; @Slf4j @Component public class MyMetaObjectHandler implements MetaObjectHandler { //插入时的填充策略 @Override public void insertFill(MetaObject metaObject) { log.info("插入"); //setFieldValByName(String fieldName, Object fieldVal, MetaObject) this.setFieldValByName("createTime", new Date(), metaObject); this.setFieldValByName("updateTime", new Date(), metaObject); // this.strictInsertFill(metaObject, "createTime", LocalDateTime::now, LocalDateTime.class); // this.strictInsertFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class); } //更新时的填充策略 @Override public void updateFill(MetaObject metaObject) { log.info("更新"); this.setFieldValByName("updateTime", new Date(), metaObject); // this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class); } }注意:该段代码中注释了另外一种写法,这是mybatis-plus 3.3.0+版本过后官方推荐的写法,但是不管怎样的写法,都要注意数据类型的一致,使用LocalDateTime的话,实体类的两个字段就要改成LocalDateTime -
执行插入操作
@Test void testInsert() { User user = new User() .setName("张三") .setAge(12) .setEmail("123@qq.com"); userMapper.insert(user); } -
再执行更新操作
@Test void testUpdate() { User user = new User() .setId(1698244309027500042L) .setName("王五") .setAge(8) .setEmail("456@gmail.com"); int update = userMapper.updateById(user); System.out.println(update); }
乐观锁(Optimistic Locking)
官方文档-乐观锁插件 | MyBatis-Plus (baomidou.com)
乐观锁是一种并发控制机制,它持有乐观的态度,相信数据冲突发生的概率较低,并允许多个任务并行地对数据进行操作,而不加锁。乐观锁的机制下,对数据的操作不会立即进行冲突检测和加锁,而是在数据提交时通过一种机制来验证是否存在冲突。乐观锁通常==通过版本号(也称为时间戳)实现==。每次读取数据时,都会获取当前版本号,并将其与修改前的版本号进行比对。如果两个版本号相同,则认为数据没有被其他任务修改,允许当前任务进行修改操作并更新版本号。如果版本号不同,则表示数据已被其他任务修改,此时需要处理冲突,通常是通过回滚操作或者给出适当的提示来解决冲突。
乐观锁:顾名思义十分乐观,它总是认为任务==不会==出现问题,无论干什么都不会上锁,如果出现问题,再次更新值测试
悲观锁:顾名思义十分悲观,它总是任务任务==总会==出现问题,无论干什么都上锁,再去操作
乐观锁实现方式:
- 取出记录时,获取当前 version
- 更新时,带上这个 version
- 执行更新时, set version = newVersion where version = oldVersion
- 如果 version 不对,就更新失败
乐观锁:先查询,获得版本号version=1
--A线程
update user set name = "张三", verison = version+1
where id = 2 and version = 1
--B线程 线程抢先完成,这个时候version = 2,会导致A修改失败!
update user set name = "张三", version = version + 1
where id = 2 and version = 1
测试Mybatis-Plus乐观锁插件
-
给数据库增加verison字段,将其==默认值设置为1==
-
User实体类添加相应的字段
@Version//这是一个乐观锁 private Integer version; -
配置拦截器组件
package com.mybatisplus.config; @Configuration @EnableTransactionManagement @MapperScan("com.mybatisplus.mapper") //可以将主启动类扫描mapper放到这儿 public class MPConfig { /** * 新版写法 * */ @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return interceptor; } /** * 旧版写法 */ // @Bean // public OptimisticLockerInterceptor optimisticLockerInterceptor() { // return new OptimisticLockerInterceptor(); // } } -
测试乐观锁==成功案例==
//测试乐观锁成功案例 @Test void testOptimisticLockSuccess() { //查询用户信息 User user = userMapper.selectById(1L); //修改用户信息 user.setAge(34).setName("丙丁"); userMapper.updateById(user); } -
测试乐观锁==失败案例==
//测试乐观锁失败案例 @Test void testOptimisticLockFail() { //模拟线程1 User user = userMapper.selectById(1L); user.setAge(34).setName("丙丁11111"); //模拟另外一个线程执行插队操作 User user2 = userMapper.selectById(1L); user2.setAge(34).setName("丙丁22222"); userMapper.updateById(user2); userMapper.updateById(user);//如果没有乐观锁,就会覆盖插队线程的值 }
查询操作
-
查询单个用户
//测试通过id查询用户 @Test void testSelectById() { User user = userMapper.selectById(1L); System.out.println(user); } -
批量查询多个用户
//测试批量查询用户 @Test void testSelectBatch() { List<User> users = userMapper.selectBatchIds(Arrays.asList(1L, 2L, 3L)); users.forEach(System.out::println); } -
条件查询map
//测试使用map进行条件查询 @Test void testSelectByMaps() { HashMap<String, Object> map = new HashMap<>(); map.put("name", "丙丁22222"); List<User> users = userMapper.selectByMap(map); users.forEach(System.out::println); }提示:如果map中有多个字段,mybatis-plus会使用and去拼接
分页查询
官方文档-分页插件 | MyBatis-Plus (baomidou.com)
几种常见的分页方式:
- 在SQL语句中使用limit语句
- pageHelper第三方插件
- MP内置的分页插件
如何使用
-
配置拦截器组件
@Configuration @EnableTransactionManagement @MapperScan("com.mybatisplus.mapper") public class MPConfig { /** * 新版写法 * */ @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); //乐观锁插件 interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); //分页插件 interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); return interceptor; } } -
直接使用Page对象
//测试分页查询 @Test void testPage() { //第一个参数:当前页 //第二个参数:页面的大小 Page<User> page = new Page<>(1,3); Page<User> userPage = userMapper.selectPage(page, null); userPage.getRecords().forEach(System.out::println); }
删除操作
-
通过id删除
//测试通过id删除 @Test void testDelete() { userMapper.deleteById(1698244309027500042L); } -
批量删除
//测试批量删除 @Test void testDeleteBatch() { userMapper.deleteBatchIds(Arrays.asList(1L, 2L, 3L)); } -
条件删除
//测试条件删除 @Test void testDeleteByMap() { HashMap<String, Object> map = new HashMap<>(); map.put("name", "小黎"); userMapper.deleteByMap(map); }
逻辑删除
官方文档-逻辑删除 | MyBatis-Plus (baomidou.com)
物理删除(硬删除):从数据库中直接删除数据
逻辑删除(软删除):在数据库中没有被删除,而是通过设置一个字段将其失效,比如一个deleted字段,为0时就是未被删除,为1就是被删除,然后在查询数据时带上deleted字段作条件查询
有些网站有管理员可以查看被删除记录的功能,它的作用就是防止数据丢失,类似于回收站
-
在数据库中增加一个deleted字段
-
实体类中增加属性
@TableLogic//逻辑删除 private Integer deleted; -
编写配置文件
#日志配置 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #逻辑删除相关配置 global-config: db-config: logic-delete-field: deleted #全局逻辑删除的实体字段名(从3.3.0+版本过后可以不用加@TableLogic注解) logic-delete-value: 1 #逻辑已删除值(默认为1) logic-not-delete-value: 0 #逻辑未删除值(默认为0)
提示:如果你用的MP版本是3.3.0+的,就不用在实体类上加@TableLogic注解
-
测试删除
@Test void testLogicDelete() { HashMap<String, Object> map = new HashMap<>(); map.put("name", "小王"); //先对数据进行逻辑删除 userMapper.deleteByMap(map); //再查询删除的数据 userMapper.selectByMap(map).forEach(System.out::println); }数据库中依然存在被逻辑删除的数据,但是最后的deleted字段变成了1
先删后查,查询时携带了deleted=0的条件,没有查询到数据自动的过滤掉了被逻辑删除的数据,其本质就是更新操作
五、🥗性能分析插件
在日常的开发中,会有一个慢SQL而我们可以使用一些分析插件进行分析
提示:MP 3.2+版已经将内置的性能分析插件移除,需要自己使用第三方的性能分析插件
此处使用p6spy
-
导入依赖
<dependency> <groupId>p6spy</groupId> <artifactId>p6spy</artifactId> <version>3.9.1</version> </dependency> -
替换MySQL8驱动
spring: datasource: #注意此处加了一个p6spy url: jdbc:p6spy:mysql://localhost:3306/mybatis_plus? characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&allowMultiQueries=true username: root password: xxxx #密码换成自己的 driver-class-name: com.p6spy.engine.spy.P6SpyDriver #换驱动 -
在resources目录下新建spy.propertis文件
modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger #日志输出到控制台 appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger # 使用日志系统记录 sql # 设置 p6spy driver 代理 deregisterdrivers=true # 取消JDBC URL前缀 useprefix=true # 配置记录 Log 例外,可去掉的结果集有error,info,batch,debug,statement,commit,rollback,result,resultset. excludecategories=info,debug,result,commit,resultset # 日期格式 dateformat=yyyy-MM-dd HH:mm:ss # 实际驱动可多个 #driverlist=org.h2.Driver # 是否开启慢SQL记录 outagedetection=true # 慢SQL记录标准 2 秒 outagedetectioninterval=1 -
测试查询全部用户
@Test void contextLoads() { userMapper.selectList(null).forEach(System.out::println); } -
效果,打印出了SQL的执行时间
六、🥩条件查询器(Wrapper)
官方文档-条件构造器 | MyBatis-Plus (baomidou.com)
复杂的SQL可以用它替代
-
查询name不为空,email不为空的用户,且age大于等于23的
@Test void test1() { //查询name不为空,email不为空的用户,且age大于等于23的 QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.isNotNull("name").isNotNull("email").ge("age",23); userMapper.selectList(wrapper); } -
通过名字查询用户,比如张三
@Test void test2() { //通过名字查询用户,比如张三 QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.eq("name","张三"); User user = userMapper.selectOne(wrapper); System.out.println(user); } -
查询年龄在15至30之间的人的数量
@Test void test3() { //查询年龄在15至30之间的人的数量 QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.between("age",15,30);//区间 Long l = userMapper.selectCount(wrapper); System.out.println(l); } -
模糊查询
@Test void test4() { QueryWrapper<User> wrapper = new QueryWrapper<>(); //name字段中不包含k,email字段左模糊查询qq.com wrapper.notLike("name","k").likeLeft("email","qq.com"); List<Map<String, Object>> maps = userMapper.selectMaps(wrapper); maps.forEach(System.out::println); }MP执行的SQL为
SELECT id,name,age,email,create_time,update_time,version,deleted FROM user WHERE deleted=0 AND (name NOT LIKE '%k%' AND email LIKE '%qq.com') -
嵌套查询
//嵌套查询 @Test void test5(){ QueryWrapper<User> wrapper = new QueryWrapper<>(); //id在子查询中查出 wrapper.inSql("id","select id from user"); List<Object> objects = userMapper.selectObjs(wrapper); objects.forEach(System.out::println); }SELECT id,name,age,email,create_time,update_time,version,deleted FROM user WHERE deleted=0 AND (id IN (select id from user)) -
降序查询
//排序查询 @Test void test6(){ QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.orderByDesc("id"); List<Object> objects = userMapper.selectObjs(wrapper); objects.forEach(System.out::println); }
七、🌮代码生成器
官方文档-代码生成器(新) | MyBatis-Plus (baomidou.com)
本人平常并未使用官方的代码生成工具而是使用的代码生成插件EasyCode