MybatisPlus
一、MybatisPlus概述
1、学习MybatisPlus前所需的知识
Mybatis、Spring、SpringMVC
2、为什么要学习MybatisPlus
Mybatis本身就是被用作简化我们CRUD过程的一个框架,而MybatisPlus是和Mybatis配合使用的,可以更加简化我们的CRUD过程,可以自动化完成CRUD。
官网原话:MyBatis-Plus(简称 MP)是一个 MyBatis的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。MybatisPlus的愿景是成为 MyBatis 最好的搭档,就像魂斗罗中的 1P、2P一样,基友搭配,效率翻倍。
3、MybatisPlus的特性
- 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
- 损耗小:启动即会自动注入基本 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 操作智能分析阻断,也可自定义拦截规则,预防误操作
4、官方文档网址
https://mp.baomidou.com/guide/
5、环境介绍
- springboot2.X
- mybatisplus3.0.5
- mysql8.0.12
- jdk1.8
6、进入文章前的提示(必读)
由于编写该文章的时候,没有使用最新的MybatisPlus版本,而目前MybatisPlus已经更新了非常多的版本了,其中很多下文设计的操作会有不同,但是总体来说是差不多的,我使用的3.0.5MP展示了更多配置细节,而最新版则省略或者改变了一些配置方式,如果你使用的是最新版的或者和我的版本差距比较大的话只需要去官方文档查看使用方法即可,基本上都是相通的。
二、Hello,MybatisPlus(入门实战)
1、学习思路
学习新组件的通用思路:
- 查找应该导入什么依赖
- 研究怎么导入,可能有版本不兼容等问题
- 尝试如何使用
- 使用成功后,研究该组件的特性和与没有该组件的时候相比的扩展/增强
2、开始入门
-
创建springboot项目(只需要勾选web即可)
-
引入依赖
这里需要mysql的依赖和mybatisplus的依赖
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.0.5</version> </dependency>
-
创建数据库并插入数据,实例数据库创建语句和插入语句如下
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)
)ENGINE=INNODB DEFAULT CHARSET=utf8 COLLATE=utf8_bin ROW_FORMAT=DYNAMIC;
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');
-
连接数据库
在application.yml或properties文件中配置数据库参数
spring:
datasource:
username: root
password: 123456
#?serverTimezone=UTC解决时区的报错
url: jdbc:mysql://localhost:3306/你的数据库名
?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
-
对比传统mybatis和使用了mybatis-plus后的区别
传统:pojo->dao(使用mybatis进行映射)->service->controller
mybatis-plus:pojo->mapper(继承BaseMapper类)->即可完成大部分简单的增删改查分页等任务
@Mapper
@Repository
//继承BaseMapper类即可
public interface UserMapper extends BaseMapper<User> {
}
@Autowired
private UserMapper userMapper;
@Test
void contextLoads() {
List<User> userList = userMapper.selectList(null);
userList.forEach(System.out::println);
}
测试结果为:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CCs02Flx-1637981062998)(C:\Users\利姆鲁\AppData\Roaming\Typora\typora-user-images\image-20211120113947137.png)]
三、配置MybatisPlus日志
1、为什么要配置日志
由于我们现在的SQL语句都是自动化的,我们看不见它的运行情况,但我们又时常希望可以看到它是用什么SQL语句来执行的还有很多运行中的其他信息,所以需要日志来帮助我们将对数据库的操作可视化。
2、配置和使用
-
在application.yml或properties文件中配置数据库参数
mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 在控制台输出,也可以用其他的日志方式
-
测试结果
我们可以看到,执行的SQL语句为:
查询到的信息为:
使用的连接和连接池为:
四、从CRUD引出MP的一些出色的功能
1.1、插入
- 我们插入时并没有设置ID,我们观察一下插入后的ID会是什么
User user = new User();
user.setName("dt");
user.setAge(3);
user.setEmail("123456@126.com");
userMapper.insert(user);
- 控制台日志
我们发现,插入的时候,mybatis-plus自动给我们设置了一个ID(主键),且这个ID肉眼看起来没有什么规律。
1.2、主键插入策略
我们在插入中,由于ID是主键,所以mp给我们自动用分布式全局唯一ID算法得到一个全局唯一ID。我们阅读文档发现,可以在POJO实体类中的主键属性上增加一个注解 @TableId ,该注解可以设置主键的生成方法/算法,具体参数如下
2.1、更新
- 更新某一行数据
User user = new User();
user.setName("faker");
user.setId(0L);
user.setEmail("123456@qq.com");
userMapper.updateById(user);
-
控制台,此时的预SQL语句和插入元素为:
-
我们还是更新这一行数据,但更新的字段不同(少了email)
User user = new User();
user.setName("rookie");
user.setId(0L);
userMapper.updateById(user);
-
控制台,此时的预SQL语句和插入元素为:
-
我们发现,我们的更新函数并没有发生变化,参数也没有发生变化,都是一个user对象,但是执行的SQL语句却不一样,它会根据我们对user赋值的情况来对SQL语句作出改变,也就是动态SQL,MP直接帮我们做了动态SQL这件事,如果使用的还是mybatis则需要我们自己手动写动态SQL,这也是MP相比传统mybatis更加方便的地方
五、自动填充
1、为什么要自动填充
我们在操作数据库表中的数据的时候,往往需要记录被操作的数据的创建时间和修改时间,方便以后查询某个数据是什么时候创建的和什么时候修改的,像阿里巴巴开发手册所说的,他们会在每个表里面加上gmt_create .gmt_modified两个字段来作为创建时间和修改时间。
而这种仅作为记录时间的字段,往往不应该每次操作的时候都调用编写代码来实现,这样过于低效,由于是固定化的任务,我们完全可以让它自动化实现,这就是为什么我们要自动填充字段。
2、mybatisplus给出的自动填充的方法
- 在需要填充的POJO实体类的字段上添加注解 @TableField ,该注解中的fill属性就用于控制该字段是否自动填充以及在什么时候填充,如:
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
- 自己编写一个实现MetaObjectHandler接口的类,该接口需要实现两个方法:insertFill和updateFill,用于控制插入和更新的时候自动填充的操作,这里主要要加**@Component**注解,否则不会注入到springboot中
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.setFieldValByName("createTime",new Date(),metaObject);
this.setFieldValByName("updateTime",new Date(),metaObject);
}
@Override
public void updateFill(MetaObject metaObject) {
this.setFieldValByName("updateTime",new Date(),metaObject);
}
}
- 测试数据
User user = new User();
user.setName("meiko");
user.setAge(10);
user.setEmail("123456@126.com");
userMapper.insert(user);
- 测试结果
- 可能遇到的问题:时间不对,这很可能是我们数据库的时差参数不对,把数据库配置中的url属性中的serverTimezone参数改为serverTimezone=GMT%2B8即可解决问题
3、另外一种自动填充的方法
也可以直接在数据库创建的时候修改字段为自动填充,或者在需要的时候,往表里添加字段的同时设置为自动填充。
但我不建议用这样的方式来做自动填充,因为真实开发环境中,往往数据库的权限不在开发方的手上,难以对数据库进行直接的修改,而且在数据库表使用后再增加字段是很大的禁忌,一般来说都是不会发生的。我们使用代码来解决问题,更加的灵活,也更加的普适,如果想要了解这种方法的话可以自动查找资料,作为了解即可。
六、乐观锁
1、什么是乐观锁
乐观锁:顾名思义十分乐观,他总是认为不会出现问题,无论干什么都不去上锁,如果出现了问题,就会再次更新值进行测试
有乐观锁也就有悲观锁,悲观锁和乐观锁相对
悲观锁:顾名思义十分悲观,他总是认为出现问题,无论干什么都会先上锁再操作
2、乐观锁的实现原理
首先需要添加乐观锁的数据库表中添加version字段,并在MP中手动乐观锁添加组件
然后:
当需要更新数据的时候,在where语句后匹配version字段是否正确,如果正确则对数据进行修改,并让version自增,否则操作失败,原SQL语句不执行(执行失败)
3、乐观锁的代码实现
1.增加version字段
@Version
private Integer version;
2.注册乐观锁
@MapperScan("com.xiafan.mapper")
@EnableTransactionManagement
@Configuration // 配置类
public class MybatisPlusConfig {
// 注册乐观锁插件
@Bean
public OptimisticLockerInterceptor optimisticLockerInterceptor(){
return new OptimisticLockerInterceptor();
}
}
3.测试乐观锁,测试串行更新和模拟多线程更新测试
//串行,可以正常运行
@Test
void testVersion1(){
User user = userMapper.selectById(1461964912795344897L);
user.setName("ququ");
user.setAge(18);
userMapper.updateById(user);;
}
//模拟多线程,user和user1对同一行数据进行更新,但在user还没更新的时候user1已经获取了原来user获取的对象了,此时两个对象version都是2,但是当user1提交后,version自增为3,再提交user时,由于此时需要验证version为2,但version已经自增为3了,所以该更新语句失效了
@Test
void testVersion2(){
User user = userMapper.selectById(1461964912795344897L);
user.setName("qq");
user.setAge(15);
User user1 = userMapper.selectById(1461964912795344897L);
user1.setName("wx");
user1.setAge(16);
userMapper.updateById(user1);
userMapper.updateById(user);
}
七、简单单表查询
1、按单个ID单个查询
2、按单个ID多个查询
3、使用Map封装键值对来帮助我们进行简单的条件查询
所调用的方法如下:
两种按ID查询的方法不必多说,重点演示一下如何进行条件查询,演示代码如下:
Map<String, Object> map = new HashMap<>();
//查询 id=1 and age=20 的数据
map.put("id",1L);
map.put("age",20);
userMapper.selectByMap(map);
八、分页查询
1、分页查询的实现方法
- 用原始sql语句中的limit关键字实现
- 使用如pageHelper这种第三方的插件实现
- 在mybatis-plus中配置分页查询插件
2、如何在mybatis-plus中配置分页查询插件
- 在配置类中配置插件,和乐观锁插件的配置相似
//注册分页插件
@Bean
public PaginationInterceptor paginationInterceptor(){
return new PaginationInterceptor();
}
- 直接使用插件内置的Page对象
@Test
void testPage(){
//第一个参数为当前页面,第二个参数为每一页的数据个数(行数)
Page<User> objectPage = new Page<>(1,5);
userMapper.selectPage(objectPage,null);
objectPage.getRecords().forEach(System.out::println);
}
九、删除
1、按单个ID单个删除
2、按单个ID多个删除
3、使用Map封装键值对来帮助我们进行简单的条件删除
所调用的方法如下:
两种按ID删除的方法不必多说,重点演示一下如何进行条件删除,演示代码如下:
Map<String, Object> map = new HashMap<>();
//查询 id=1 and age=20 的数据
map.put("id",0L);
map.put("name","xiaohu");
userMapper.deleteByMap(map);
十、逻辑删除
1、什么是逻辑删除
逻辑删除之所以叫逻辑删除,是因为它和物理删除这种真实删除不一样,区别如下:
物理删除:从存储介质中完全抹去了这个数据,这个数据永远的删除了,无法再得到它
逻辑删除:我们通过某些方法,比如增加一个字段,当字段=0的时候,默认它是存在的,当字段=1的时候,默认它被删除了,但是它其实并没有被删除,只是在做其他操作的时候会被忽略掉,只是在“逻辑上”被删除了,所以被叫做逻辑删除
2、如何实现逻辑删除功能
逻辑删除的实现和乐观锁的实现非常相似。
- 在数据库中添加一个deleted字段,默认值置为0(最好这么做,后面会进行配置,可以根据自己的需要改,但是建议使用0)
- 在实体类中添加deleted字段
@TableLogic
private Integer deleted;
- 和乐观锁插件、分页插件类似,在配置类中注册插件
//注册逻辑删除插件
@Bean
public ISqlInjector sqlInjector(){
return new LogicSqlInjector();
}
- 在springboot的yml文件中编写逻辑删除的相关配置
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 在控制台输出,也可以用其他的日志方式
global-config:
db-config:
logic-delete-value: 1
logic-not-delete-value: 0
-
测试逻辑删除功能
3、总结
本质上,逻辑删除就是把”删除“变为了”更新“,我们只要增加一个标记字段,然后约定好这个字段为某个值代表它应该被”看见“,为某个别的值代表它不能被”看见“,就可以实现看起来好像删除了,但其实数据还存在我们的存储介质中的效果
十一、条件构造器
1、什么是条件构造器
条件构造器是用来让MP能够进行复杂SQL操作的工具,如模糊查询,分组查询等,这些复杂操作只使用MP的BaseMapper里已经封装好的方法是不够用的(不加Wrapper类参数的话)
2、怎么使用条件构造器
条件构造器的使用是建立在Wrapper类上的,Wrapper类封装了绝大部分SQL语句的操作,且支持链式编程(即可以连续设置参数)
实例:这里只实例一部分方法,其他的去看官方文档即可,用法简单易懂
//>=
@Test
void test1(){
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper
.ge("age","28")
.isNotNull("name")
.select("name");
userMapper.selectList(wrapper).forEach(System.out::println);
}
//between
@Test
void test2(){
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper
.between("age",20,30);
List<User> userList = userMapper.selectList(wrapper);
for (User user : userList){
System.out.println(user.getName());
}
}
//模糊查询like
@Test
void test3(){
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper
.likeRight("name","J")
.notLike("name","a");
List<User> userList = userMapper.selectList(wrapper);
for (User user : userList){
System.out.println(user.getName());
}
}
十二、代码自动生成器
本质是使用MP封装好的类对所需生成的代码文件进行配置,这一块没有什么技术上的要求,这里使用官方文档的例子作为说明:
// 演示例子,执行 main 方法控制台输入模块表名回车自动生成对应项目目录中
public class CodeGenerator {
/**
* <p>
* 读取控制台内容
* </p>
*/
public static String scanner(String tip) {
Scanner scanner = new Scanner(System.in);
StringBuilder help = new StringBuilder();
help.append("请输入" + tip + ":");
System.out.println(help.toString());
if (scanner.hasNext()) {
String ipt = scanner.next();
if (StringUtils.isNotBlank(ipt)) {
return ipt;
}
}
throw new MybatisPlusException("请输入正确的" + tip + "!");
}
public static void main(String[] args) {
// 代码生成器
AutoGenerator mpg = new AutoGenerator();
// 全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
gc.setOutputDir(projectPath + "/src/main/java");
gc.setAuthor("jobob");
gc.setOpen(false);
// gc.setSwagger2(true); 实体属性 Swagger2 注解
mpg.setGlobalConfig(gc);
// 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://localhost:3306/ant?useUnicode=true&useSSL=false&characterEncoding=utf8");
// dsc.setSchemaName("public");
dsc.setDriverName("com.mysql.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("密码");
mpg.setDataSource(dsc);
// 包配置
PackageConfig pc = new PackageConfig();
pc.setModuleName(scanner("模块名"));
pc.setParent("com.baomidou.ant");
mpg.setPackageInfo(pc);
// 自定义配置
InjectionConfig cfg = new InjectionConfig() {
@Override
public void initMap() {
// to do nothing
}
};
// 如果模板引擎是 freemarker
String templatePath = "/templates/mapper.xml.ftl";
// 如果模板引擎是 velocity
// String templatePath = "/templates/mapper.xml.vm";
// 自定义输出配置
List<FileOutConfig> focList = new ArrayList<>();
// 自定义配置会被优先输出
focList.add(new FileOutConfig(templatePath) {
@Override
public String outputFile(TableInfo tableInfo) {
// 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
return projectPath + "/src/main/resources/mapper/" + pc.getModuleName()
+ "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
}
});
/*
cfg.setFileCreate(new IFileCreate() {
@Override
public boolean isCreate(ConfigBuilder configBuilder, FileType fileType, String filePath) {
// 判断自定义文件夹是否需要创建
checkDir("调用默认方法创建的目录,自定义目录用");
if (fileType == FileType.MAPPER) {
// 已经生成 mapper 文件判断存在,不想重新生成返回 false
return !new File(filePath).exists();
}
// 允许生成模板文件
return true;
}
});
*/
cfg.setFileOutConfigList(focList);
mpg.setCfg(cfg);
// 配置模板
TemplateConfig templateConfig = new TemplateConfig();
// 配置自定义输出模板
//指定自定义模板路径,注意不要带上.ftl/.vm, 会根据使用的模板引擎自动识别
// templateConfig.setEntity("templates/entity2.java");
// templateConfig.setService();
// templateConfig.setController();
templateConfig.setXml(null);
mpg.setTemplate(templateConfig);
// 策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setNaming(NamingStrategy.underline_to_camel);
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
strategy.setSuperEntityClass("你自己的父类实体,没有就不用设置!");
strategy.setEntityLombokModel(true);
strategy.setRestControllerStyle(true);
// 公共父类
strategy.setSuperControllerClass("你自己的父类控制器,没有就不用设置!");
// 写于父类中的公共字段
strategy.setSuperEntityColumns("id");
strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
strategy.setControllerMappingHyphenStyle(true);
strategy.setTablePrefix(pc.getModuleName() + "_");
mpg.setStrategy(strategy);
mpg.setTemplateEngine(new FreemarkerTemplateEngine());
mpg.execute();
}
}