1、MyBatisPlus概述
需要的基础
-
MyBatis
-
Spring
-
SpringMVC
为什么要学习它呢?MyBatisPlus可以节省我们大量工作时间,所有的CRUD代码它都可以自动化完成!
还有类似的
-
JPA
-
tk-mapper
简介
就是用来简化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 操作智能分析阻断,也可自定义拦截规则,预防误操作
2、快速入门
地址:https://baomidou.com/pages/226c21/#初始化工程
-
新建数据库
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) );
-
初始化
user
表数据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项目
springboot-mybatis-plus
-
导包
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <!-- mybatis-plus 是自己开发,并非官方的! --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.0.5</version> </dependency>
-
配置数据源
application.properties
spring.datasource.username=root spring.datasource.password=123456 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver # mysql 5 驱动不同 com.mysql.jdbc.Driver # mysql 8 驱动不同com.mysql.cj.jdbc.Driver、需要增加时区的配置 spring.datasource.url=jdbc:mysql://localhost:3306/mybatis-plus?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
-
编写代码
传统方式:pojo--dao(配置mapper.xml文件) 【这里省略】
使用mybatis-plus之后
-
编写pojo
User.java
package com.zyy.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** * @Description: 类描述 * @Author: zyy * @Date: 2022/12/07 22:42 */ @Data @NoArgsConstructor @AllArgsConstructor public class User { private Long id; private String name; private Integer age; private String email; }
-
编写mapper
UserMapper.java
package com.zyy.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.zyy.pojo.User; import org.springframework.stereotype.Repository; /** * 在对应的Mapper上面继承基本的类 BaseMapper * * @Repository 代表持久层 */ @Repository public interface UserMapper extends BaseMapper<User> { // 所有的CRUD操作都已经编写完成了 // 你不需要像以前的配置一大堆文件了 }
-
启动类添加扫描我们的mapper
package com.zyy; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @MapperScan("com.zyy.mapper") public class SpringbootMybatisPlusApplication { public static void main(String[] args) { SpringApplication.run(SpringbootMybatisPlusApplication.class, args); } }
-
-
测试
package com.zyy; import com.zyy.mapper.UserMapper; import com.zyy.pojo.User; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.util.List; @SpringBootTest class SpringbootMybatisPlusApplicationTests { /** * 继承了BaseMapper,所有的方法都来自己父类 * 我们也可以编写自己的扩展方法! */ @Autowired private UserMapper userMapper; @Test void contextLoads() { // 参数是一个 Wrapper ,条件构造器,这里我们先不用 null // 查询全部用户 List<User> userList = userMapper.selectList(null); userList.forEach(user -> System.out.println(user)); } }
3、配置日志输出
application.properties
# 配置日志 配置控制台打印 mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
重新运行上面的测试类,就可以看到上面查询详情了!
4、CRUD扩展
插入操作
insert插入
@Test void addUser(){ User user = new User(); user.setName("张三"); user.setAge(18); user.setEmail("123456@qq.com"); int count = userMapper.insert(user); System.out.println(count);//受影响行数 System.out.println(user);//打印后发现id是自动回填的,注意这里字段id并没有设置自增和默认值 }
数据库插入的id的默认值为:全局的唯一id
主键生成策略
默认 ID_WORKER 全局唯一id
分布式系统唯一id生成:https://segmentfault.com/a/1190000020993874
雪花算法:
核心思想:把64-bit分别划分成多段,分开来标示机器、时间、某一并发序列等,从而使每台机器及同一机器生成的ID都是互不相同。
PS:这种结构是雪花算法提出者Twitter的分法,但实际上这种算法使用可以很灵活,根据自身业务的并发情况、机器分布、使用年限等,可以自由地重新决定各部分的位数,从而增加或减少某部分的量级。比如:百度的UidGenerator、美团的Leaf等,都是基于雪花算法做一些适合自身业务的变化。
优点:
-
整体上按照时间按时间趋势递增,后续插入索引树的时候性能较好。
-
整个分布式系统内不会产生ID碰撞(由数据中心标识ID、机器标识ID作区分)
-
本地生成,且不依赖数据库(或第三方组件),没有网络消耗,所以效率高(每秒能够产生26万ID左右)。
缺点:
-
由于雪花算法是强依赖于时间的,在分布式环境下,如果发生时钟回拨,很可能会引起ID重复、ID乱序、服务会处于不可用状态等问题。
解决方案有:
-
将ID生成交给少量服务器,并关闭时钟同步。
-
直接报错,交给上层业务处理。
-
如果回拨时间较短,在耗时要求内,比如5ms,那么等待回拨时长后再进行生成。
-
如果回拨时间很长,那么无法等待,可以匀出少量位(1~2位)作为回拨位,一旦时钟回拨,将回拨位加1,可得到不一样的ID,2位回拨位允许标记3次时钟回拨,基本够使用。如果超出了,可以再选择抛出异常。
主键自增
-
在实体类字段上加上
@TableId(type = IdType.AUTO)
@Data @NoArgsConstructor @AllArgsConstructor public class User { @TableId(type = IdType.AUTO) private Long id; private String name; private Integer age; private String email; }
-
数据库字段一定要是自增
-
再次测试插入即可
IdType类型解释
public enum IdType { AUTO(0),//数据库id自增 NONE(1),//未设置主键 INPUT(2),//手动输入 ID_WORKER(3),//默认的全局唯一id UUID(4),//全局唯一id uuid ID_WORKER_STR(5);//ID_WORKER 字符串表示法 //... }
更新操作
@Test void updateUser(){ User user = new User(); // 通过条件自动拼接动态sql user.setId(6L); user.setName("李四"); //user.setAge(18); user.setEmail("123456@qq.com"); // 注意:updateById 但是参数是一个 对象! int count = userMapper.updateById(user); System.out.println(count); System.out.println(user); }
先把user.setAge(18);
注释了,然后再打开
执行截图如下:
可以看出来sql是自动帮你动态配置的!
自动填充
创建时间、修改时间!这些个操作一遍都是自动化完成的,我们不希望手动更新!
阿里巴巴开发手册:所有的数据库表:gmt_create、gmt_update几乎所有的表都要配置上!而且需 要自动化!
方式一:数据库级别
-
在表中新增字段gmt_create、gmt_update
-
把对应实体类也加上
@Data @NoArgsConstructor @AllArgsConstructor public class User { @TableId(type = IdType.AUTO) private Long id; private String name; private Integer age; private String email; private Date gmtCreate; private Date gmtUpdate; }
-
再次更新查看结果即可
方式二:代码级别
-
删除数据库的默认值、更新操作
-
实体类字段属性上需要增加注释
@Data @NoArgsConstructor @AllArgsConstructor public class User { @TableId(type = IdType.AUTO) private Long id; private String name; private Integer age; private String email; @TableField(fill = FieldFill.INSERT) private Date gmtCreate; @TableField(fill = FieldFill.INSERT_UPDATE) private Date gmtUpdate; }
-
编写处理器来处理这个注解即可MyMetaObjectHandler.java
package com.zyy.handler; import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.reflection.MetaObject; import org.springframework.stereotype.Repository; import java.util.Date; /** * @Description: 类描述 * @Author: zyy * @Date: 2023/01/30 22:42 */ @Slf4j @Repository public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { log.info("start insert fill"); //setFieldValByName(String fieldName, Object fieldVal, MetaObject metaObject) this.setFieldValByName("gmtCreate", new Date(), metaObject); this.setFieldValByName("gmtUpdate", new Date(), metaObject); } @Override public void updateFill(MetaObject metaObject) { log.info("start update fill"); this.setFieldValByName("gmtUpdate", new Date(), metaObject); } }
-
测试插入
-
测试更新,观察gmt_create、gmt_update情况
乐观锁处理讲解
-
乐观锁:乐观锁在操作数据时非常乐观,认为别人不会同时修改数据。因此乐观锁不会上锁,只是在执行更新的时候判断一下在此期间别人是否修改了数据:如果别人修改了数据则放弃操作,否则执行操作。
-
悲观锁:悲观锁在操作数据时比较悲观,认为别人会同时修改数据。因此操作数据时直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据。
乐观锁的实现方式:
-
取出记录时,获取当前version
-
更新时,更新条件带上version
-
执行更新时,set version = newVersion where version = oldVersion
-
如果version不对,就更新失败
测试一下乐观锁
-
给表添加version列
-
给实体类添加version字段,并添加
@Version
注解@Data @NoArgsConstructor @AllArgsConstructor public class User implements Serializable { @TableId(type = IdType.AUTO) private Long id; private String name; private Integer age; private String email; @TableField(fill = FieldFill.INSERT) private Date gmtCreate; @TableField(fill = FieldFill.INSERT_UPDATE) private Date gmtUpdate; @Version private Integer version; }
-
注册组件
import com.baomidou.mybatisplus.extension.plugins.OptimisticLockerInterceptor; import org.mybatis.spring.annotation.MapperScan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.transaction.annotation.EnableTransactionManagement; /** * @Description: 类描述 * @Author: zyy * @Date: 2023/01/31 21:52 */ @EnableTransactionManagement @MapperScan("com.zyy.mapper") @Configuration public class MyBatisPlusConfig { @Bean public OptimisticLockerInterceptor optimisticLockerInterceptor() { return new OptimisticLockerInterceptor(); } }
-
测试一下
@Test void optimisticLocker() { User user = new User(); user.setId(1L); //模拟线程A先查询用户1 User aUser = userMapper.selectById(user); //模拟线程B也查询用户1 User bUser = userMapper.selectById(user); //模拟线程B更新了表 bUser.setAge(31); int i = userMapper.updateById(bUser); System.out.println(i); //模拟线程A后面更新表 这里应该更新失败 aUser.setAge(41); int j = userMapper.updateById(aUser); System.out.println(j); }
-
结果:线程B更新成功,线程A更新失败(原因是:线程B更新的时候,version已经+1了,再用原来的version的值,条件就不符合了)
查询操作
//测试查询 @Test void selectById(){ User user = userMapper.selectById(1L); System.out.println(user); } //测试批量查询 @Test void selectBatchIds(){ List<User> userList = userMapper.selectBatchIds(Arrays.asList(1, 2, 3)); userList.forEach(System.out::println); } //测试条件查询之一 map操作 @Test void selectByMap(){ Map<String, Object> columnMap = new HashMap<>(); columnMap.put("age", 18); List<User> userList = userMapper.selectByMap(columnMap); userList.forEach(System.out::println); }
分页查询
-
原始的limit进行分页
-
pageHelper第三方分页插件
-
MP其实也内置了分页插件
如何使用MP内置的分页插件??
-
配置拦截器组件
@EnableTransactionManagement @MapperScan("com.zyy.mapper") @Configuration public class MyBatisPlusConfig { //... @Bean public PaginationInterceptor paginationInterceptor() { return new PaginationInterceptor(); } }
-
直接使用Page对象操作
@Test void page() { /** * 参数1:当前页 * 参数2:页面大小 */ Page<User> page = new Page<>(2,5); userMapper.selectPage(page, null); page.getRecords().forEach(System.out::println); }
可以看到先查询了总数,然后分页查询
删除操作
@Test void delete(){ userMapper.deleteById(1619999093600550914L); } @Test void deleteBatchIds(){ userMapper.deleteBatchIds(Arrays.asList(1619999093600550915L, 1619999093600550916L)); } @Test void deleteByMap(){ Map<String, Object> columnMap = new HashMap<>(); columnMap.put("age", 18); userMapper.deleteByMap(columnMap); }
逻辑删除
物理删除 :从数据库中直接移除
逻辑删除 :再数据库中没有被移除,而是通过一个变量来让他失效! deleted = 0 => deleted = 1
管理员可以查看被删除的记录!防止数据的丢失,类似于回收站!
实现
-
在数据表中增加一个 deleted 字段
-
实体类中增加属性
@Data @NoArgsConstructor @AllArgsConstructor public class User implements Serializable { @TableId(type = IdType.AUTO) private Long id; private String name; private Integer age; private String email; @TableField(fill = FieldFill.INSERT) private Date gmtCreate; @TableField(fill = FieldFill.INSERT_UPDATE) private Date gmtUpdate; @Version private Integer version; @TableLogic //逻辑删除 private Integer deleted; }
-
配置组件和配置项
@EnableTransactionManagement @MapperScan("com.zyy.mapper") @Configuration public class MyBatisPlusConfig { //... @Bean public ISqlInjector sqlInjector() { return new LogicSqlInjector(); } }
# 配置逻辑删除 mybatis-plus.global-config.db-config.logic-delete-value=1 mybatis-plus.global-config.db-config.logic-not-delete-value=0
-
测试一下删除
@Test void delete(){ userMapper.deleteById(1619999093600550919L); }
观察结果
发现执行的是更新操作,不是删除操作!
再调用一下查询
@Test void selectById(){ User user = userMapper.selectById(1L); System.out.println(user); }
发现会自动带上deleted=0的条件,被删除的就查不到了。
性能分析插件
我们在平时的开发中,会遇到一些慢sql。
作用:性能分析拦截器,用于输出每条 SQL 语句及其执行时间
MP也提供性能分析插件,如果超过这个时间就停止运行!
使用
-
导入插件
@EnableTransactionManagement @MapperScan("com.zyy.mapper") @Configuration public class MyBatisPlusConfig { //.... /** * SQL执行效率插件 */ @Bean @Profile({"dev","test"})// 设置 dev test 环境开启,保证我们的效率 public PerformanceInterceptor performanceInterceptor() { PerformanceInterceptor performanceInterceptor = new PerformanceInterceptor(); performanceInterceptor.setMaxTime(100); // ms 设置sql执行的最大时间,如果超过了则不执行 performanceInterceptor.setFormat(true); // 是否格式化代码 return performanceInterceptor; } }
记住,要在springboot中配置环境为dev或者test环境
# 配置环境为dev spring.profiles.active=dev
-
测试使用
@Test void contextLoads() { // 参数是一个 Wrapper ,条件构造器,这里我们先不用 null // 查询全部用户 List<User> userList = userMapper.selectList(null); userList.forEach(user -> System.out.println(user)); }
上面超过了设置的时间就会报错,如设置30ms的话,报错如下:
条件构造器
十分重要:Wrapper
我们写一些复杂的sql就可以使用它来替代!
测试
-
测试1:记住查看输出的SQL进行分析
@Test void test1() { QueryWrapper wrapper = new QueryWrapper(); wrapper.isNotNull("name"); wrapper.isNotNull("email"); wrapper.ge("age", 30); //可以和我们刚刚学的map对比一下 userMapper.selectList(wrapper).forEach(System.out::println); }
-
测试2:记住查看输出的SQL进行分析
@Test void test2() { QueryWrapper wrapper = new QueryWrapper(); wrapper.eq("age", 20); //可以和我们刚刚学的map对比一下 userMapper.selectList(wrapper).forEach(System.out::println); }
-
测试3:记住查看输出的SQL进行分析
@Test void test3() { QueryWrapper wrapper = new QueryWrapper(); //相当于age BETWEEN 20 AND 50 wrapper.between("age", 20, 50); //可以和我们刚刚学的map对比一下 System.out.println(userMapper.selectCount(wrapper));//查询结果数 }
-
测试4:记住查看输出的SQL进行分析
@Test void test41() { QueryWrapper wrapper = new QueryWrapper(); //相当于name NOT LIKE '%J%' wrapper.notLike("name", "J"); //可以和我们刚刚学的map对比一下 userMapper.selectMaps(wrapper).forEach(System.out::println); } @Test void test42() { QueryWrapper wrapper = new QueryWrapper(); //相当于name LIKE 'J%' wrapper.likeRight("name", "J"); //可以和我们刚刚学的map对比一下 userMapper.selectObjs(wrapper).forEach(System.out::println); }
-
测试5:记住查看输出的SQL进行分析
@Test void test5() { QueryWrapper wrapper = new QueryWrapper(); //相当于id IN (select id from user where id<5) wrapper.inSql("id", "select id from user where id<5"); //可以和我们刚刚学的map对比一下 userMapper.selectList(wrapper).forEach(System.out::println); }
-
测试6:记住查看输出的SQL进行分析
@Test void test6() { QueryWrapper wrapper = new QueryWrapper(); //相当于ORDER BY id ASC wrapper.orderByAsc("id"); //可以和我们刚刚学的map对比一下 userMapper.selectList(wrapper).forEach(System.out::println); }
代码自动生成器
dao、pojo、service、controller都给我自己去编写完成!
AutoGenerator 是 MyBatis-Plus 的代码生成器,通过 AutoGenerator 可以快速生成 Entity、 Mapper、Mapper XML、Service、Controller 等各个模块的代码,极大的提升了开发效率。
测试(下面代码可以按需修改)
package com.zyy; import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.annotation.FieldFill; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.generator.AutoGenerator; import com.baomidou.mybatisplus.generator.config.DataSourceConfig; import com.baomidou.mybatisplus.generator.config.GlobalConfig; import com.baomidou.mybatisplus.generator.config.PackageConfig; import com.baomidou.mybatisplus.generator.config.StrategyConfig; import com.baomidou.mybatisplus.generator.config.po.TableFill; import com.baomidou.mybatisplus.generator.config.rules.DateType; import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy; import java.util.ArrayList; // 代码自动生成器 public class ZYYCode { public static void main(String[] args) { // 需要构建一个 代码自动生成器 对象 AutoGenerator mpg = new AutoGenerator(); // 配置策略 // 1、全局配置 GlobalConfig gc = new GlobalConfig(); String projectPath = System.getProperty("user.dir"); gc.setOutputDir(projectPath + "/src/main/java");//生成文件的输出目录【默认 D 盘根目录】 gc.setAuthor("zyy"); gc.setOpen(false);//是否打开输出目录 gc.setFileOverride(false); // 是否覆盖已有文件 gc.setServiceName("%sService"); // 各层文件名称方式,例如: %sAction 生成 UserAction gc.setIdType(IdType.ID_WORKER);// 指定生成的主键的ID类型 gc.setDateType(DateType.ONLY_DATE);//时间类型对应策略 gc.setSwagger2(true); mpg.setGlobalConfig(gc); //2、设置数据源 DataSourceConfig dsc = new DataSourceConfig(); dsc.setUrl("jdbc:mysql://localhost:3306/kuang_community?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8"); dsc.setDriverName("com.mysql.cj.jdbc.Driver"); dsc.setUsername("root"); dsc.setPassword("123456"); dsc.setDbType(DbType.MYSQL); mpg.setDataSource(dsc); //3、包的配置 PackageConfig pc = new PackageConfig(); pc.setModuleName("blog");//父包模块名 pc.setParent("com.kuang");//父包名。如果为空,将下面子包名必须写全部, 否则就只需写子包名 pc.setEntity("entity");//Entity包名 pc.setMapper("mapper");//Mapper包名 pc.setService("service");//Service包名 pc.setController("controller");//Controller包名 mpg.setPackageInfo(pc); //4、策略配置 StrategyConfig strategy = new StrategyConfig(); strategy.setInclude("blog_tags", "course", "links", "sys_settings", "user_record", "user_say"); // 需要包含的表名,允许正则表达式(与exclude二选一配置) strategy.setNaming(NamingStrategy.underline_to_camel);//数据库表映射到实体的命名策略 strategy.setColumnNaming(NamingStrategy.underline_to_camel);//数据库表字段映射到实体的命名策略 strategy.setEntityLombokModel(true); // 【实体】是否为lombok模型(默认 false) strategy.setLogicDeleteFieldName("deleted");//逻辑删除属性名称 // 自动填充配置 TableFill gmtCreate = new TableFill("gmt_create", FieldFill.INSERT); TableFill gmtModified = new TableFill("gmt_modified", FieldFill.INSERT_UPDATE); ArrayList<TableFill> tableFills = new ArrayList<>(); tableFills.add(gmtCreate); tableFills.add(gmtModified); strategy.setTableFillList(tableFills);//表填充字段 // 乐观锁 strategy.setVersionFieldName("version");//设置乐观锁字段 strategy.setRestControllerStyle(true);//生成 <code>@RestController</code> 控制器 strategy.setControllerMappingHyphenStyle(true); //localhost:8080/hello_id_2 mpg.setStrategy(strategy); mpg.execute(); //执行 } }