封面画师:T5-茨舞(微博) 封面ID:69983911
1. MyBatis-Plus 概述
1.1 为什么要学习?
Hibernate 是一种全自动化的 ORM 框架,但是 MyBatis 不是,人们认为 MyBatis 属于半自动化的 ORM 框架,因为需要手写 SQL 语句。针对复杂的 SQL 进行手写倒也还好,但是一些简单的 SQL 还要手写就变得十分麻烦(我知道有注解,闭嘴!),同时还会让 XML 映射文件变得十分臃肿,那么有没有方法可以省略简单 SQL 的书写呢?
这时候,一个「天降猛男」登场了——MyBatis-Plus。💪
MyBatis-Plus 可以很好地帮我们简化操作,对于一些简单的 CRUD 操作,不再需要我们编写。当然,除了这个最「肤浅」的作用外,MyBatis-Plus 还有很多其他的特性,总之就是一句话:为了简化操作,为了偷懒。😂
MyBatis-Plus 由国人开发,文档很详细,编码符合国人习惯,荣获【2019 年度开源中国最受欢迎软件,开发工具类 TOP1】,还能简化开发,所以…不学一个?
PS:本文中,MP 是 MyBatis-Plus 的简写。
1.2 MyBatis-Plus 概述
官网:MyBatis-Plus
MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。

基友搭配,效率翻倍! 😆
1.3 特性
- 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
- 损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作
- 强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求 ( 言外之意,简单的 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. 快速开始
官方快速开始地址:MyBatis-Plus-QuickStart
拓展使用第三方组件的步骤:
- 导入对应的依赖
- 编写对应的配置
- 研究代码的编写
- 提高技术的拓展
步骤
1、创建一个数据库 mybatis_plus
2、创建 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_created、gmt_modified
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');

3、使用 IDEA 创建一个 SpringBoot 项目 mybatis_plus,选择 web 和 lombok 依赖进行导入。
4、导入其他依赖
<!--数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- mybatis-plus -->
<!-- 3.0.3版本移除 对 mybatis-plus-generator 包(代码生成器使用)的依赖,自己按需引入 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>Latest Version</version>
</dependency>
说明:使用 MyBatis-Plus 可以节省代码的编写,尽量 不要同时 导入 MyBatis-Plus 和 MyBatis,避免存在依赖错误。
5、连接数据库
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis_plus?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
没使用 MyBatis-Plus 时,我们需要编写 pojo、dao、service、controller,但是使用了 Mybatis-Plus 后,就不需要这么麻烦了。
6、使用 Mybatis-Plus 后:
- pojo
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Long id; // 对应数据库的主键
private String name;
private Integer age;
private String email;
}
- mapper 接口
// 使用Mybatis-Plus后,只需要在对应的Mapper上继承BaseMapper即可
@Repository // 表示持久层
public interface UserMapper extends BaseMapper<User> {
// 这个时候,简单的CURD已经编写完成了
}
注意:编写完 mapper 接口后,还需要在主启动类上扫描 mapper 文件夹: @MapperScan("com.yang.mapper")
- 编写测试类进行测试
@SpringBootTest
class MybatisPlusApplicationTests {
@Autowired
private UserMapper userMapper;
@Test
void contextLoads() {
// 查询全部用户
// selectList的参数是Wrapper,这是一个条件构造器
List<User> users = userMapper.selectList(null);
users.forEach(System.out::println);
}
}
查询结果:
思考
- 我们没有编写 SQL,是怎么查询出来的?MyBatis-Plus 已经帮我们写好了
- SQL 操作的方法又是拿来的?MyBatis-Plus 已经帮我们写好了
3. 配置日志
使用 MyBatis-Plus 后,部分 SQL 是不可见的,我们希望知道它是如何执行的,这个时候就需要查看日志!
在配置文件中进行配置:
# MyBatis-Plus配置日志
# 使用默认控制台输出,不需要导入依赖
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
配置日志后,在以后的学习中,我们可以查看日志,观察 MyBatis-Plus 的 SQL 执行。
4. 数据基本操作
所有操作,均在测试类中进行。
4.1 数据插入
@Test
public void testInsert(){
User user = new User();
// 注意:这里没有插入id
user.setName("默烦");
user.setAge(18);
user.setEmail("cy.mofan@qq.com");
int result = userMapper.insert(user);
// 受影响的行数
System.out.println(result);
System.out.println(user);
}
然后运行测试:

我们发现:我们明明没有设置 ID,插入时居然自动生成了 ID,而且生成的 ID 似乎有点「 奇怪 」。
数据库插入的 ID 应该是全局唯一的,这个时候我们需要了解一些主键生成策略。👇
4.2 主键生成策略
可以使用 UUID、自增 ID、雪花算法、Redis、ZK 等方式来生成主键。
默认 ID_WORKER 全局唯一 ID
参考链接:分布式系统唯一ID生成方案汇总
MyBatis-Plus 默认采用的使用推特的 雪花算法。
雪花算法:
snowflake 是 Twitter 开源的分布式 ID 生成算法,结果是一个 long 型的 ID。其核心思想是:使用 41bit 作为毫秒数,10bit 作为机器的 ID(5 个 bit 是数据中心,5 个 bit 的机器 ID),12bit 作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是 0。
我们可以前往我们的 User 实体类,然后在 id 属性上加一个注解:@TableId,点击进入这个注解:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface TableId {
String value() default "";
IdType type() default IdType.NONE;
}
点击并进入 Idtype:
public enum IdType {
AUTO(0), // 主键自增
NONE(1), // 未设置主键
INPUT(2), // 手动输入
ID_WORKER(3), // 默认,雪花算法
UUID(4), // UUID
ID_WORKER_STR(5); // ID_WORKER的字符串表示法
private int key;
private IdType(int key) {this.key = key;}
public int getKey() {return this.key;}
}
我们发现这是一个枚举类,其中的 ID_WORKER 就是默认生成策略。
主键自增
使用 MyBatis-Plus,设置组件生成策略为 自增 的方法:
1、实体类中表示组件的属性上添加注解:@TableId(type = IdType.AUTO)
2、数据库字段一定要是自增的
3、注解添加完成、数据库字段修改(记得保存)都完成后,我们在此运行插入测试:
手动输入
使用 MyBatis-Plus,设置组件生成策略为 手动输入 的方法:
只需要在实体类中表示组件的属性上添加注解:@TableId(type = IdType.AUTO) 即可。
如果我们设置组件生成策略为 手动输入 ,但是没有输入,这个时候日志就会显示插入的是 null。
@Test
public void testInsert(){
User user = new User();
// user.setName("默烦");
user.setId(6L);
user.setName("mofan");
user.setAge(18);
user.setEmail("cy.mofan@qq.com");
int result = userMapper.insert(user);
// 受影响的行数
System.out.println(result);
System.out.println(user);
}
运行结果:
4.3 数据更新
更新单个数据
根据 ID 更新单个数据:
@Test
public void testUpdate(){
User user = new User();
user.setId(6L);
user.setName("Yang");
// updateById 的参数类型是对象!
int result = userMapper.updateById(user);
System.out.println(result);
System.out.println(user);
}
控制台日志打印:

数据库显示:
更新多个数据
根据 ID 更新多个数据:
@Test
public void testUpdate(){
User user = new User();
user.setId(6L);
user.setName("mofan");
user.setAge(20);
int result = userMapper.updateById(user);
System.out.println(result);
System.out.println(user);
}
控制台日志打印:

数据库显示:
MP(以下 MyBatis-Plus 简写为 MP)会自动根据给对象设置的属性值来更新数据。
更新字段值为
null
有两种可供方式可供参考:
- 使用
LambdaUpdateWrapper - 使用
UpdateWrapper
现有如下的实体类:
@Getter
@Setter
@TableName("tb_test_null")
public class TestNull extends BaseEntity {
@Serial
private static final long serialVersionUID = -1332169884425770306L;
@TableField("name")
private String name;
@TableField("age")
private Integer age;
@TableField("empty_field")
private String emptyField;
}
先插入一条数据:
private static final String UNIQUE_NAME = "mofan";
@BeforeEach
public void init() {
TestNull testNull = new TestNull();
testNull.setName(UNIQUE_NAME);
testNull.setAge(21);
testNull.setEmptyField("");
testNullDao.insert(testNull);
}
然后尝试将 age 和 emptyField 值更新为 null:
private List<TestNull> getMofanTestNull() {
LambdaQueryWrapper<TestNull> wrapper = Wrappers.lambdaQuery(TestNull.class)
.eq(TestNull::getName, UNIQUE_NAME);
return testNullDao.selectList(wrapper);
}
@Test
public void testLambdaWrapperUpdateNull() {
Optional<TestNull> optional = getMofanTestNull().stream()
.filter(i -> UNIQUE_NAME.equals(i.getName()))
.findFirst();
var wrapper = Wrappers.lambdaUpdate(TestNull.class)
.set(TestNull::getEmptyField, null)
.set(TestNull::getAge, null);
optional.ifPresent(i -> testNullDao.update(i, wrapper));
optional = getMofanTestNull().stream()
.filter(i -> UNIQUE_NAME.equals(i.getName()))
.findFirst();
optional.ifPresent(
entity -> assertThat(entity)
.extracting(TestNull::getEmptyField, TestNull::getAge)
.containsOnlyNulls()
);
}
@Test
public void testWrapperUpdateNull() {
Optional<TestNull> optional = getMofanTestNull().stream()
.filter(i -> UNIQUE_NAME.equals(i.getName()))
.findFirst();
if (optional.isEmpty()) {
Assertions.fail();
}
TestNull entity = optional.get();
var wrapper = new UpdateWrapper<>(entity);
wrapper.set("empty_field", null).set("age", null);
testNullDao.update(entity, wrapper);
optional = getMofanTestNull().stream()
.filter(i -> UNIQUE_NAME.equals(i.getName()))
.findFirst();
optional.ifPresent(
i -> assertThat(i)
.extracting(TestNull::getEmptyField, TestNull::getAge)
.containsOnlyNulls()
);
}
4.4 自动填充
创建时间、更新时间,对于这两个字段的操作我们希望是自动完成而不是需要手动编写。
阿里巴巴开发手册说过:任何数据库表都必须有两个字段:gmt_created(创建时间)、gmt_modified(修改时间),而且需要这两个字段的更新自动化。
自动填充有两种方式:数据库级别、代码级别
数据库级别(实际开发不允许)
1、在表中新增两个字段 create_time、update_time,并将这两个字段类型设置为 timestamp,开启根据当前时间戳更新:
2、同步实体类:
// 数据库使用下划线,实体类使用驼峰命名,依旧可以进行匹配
private Date createTime;
private Date updateTime;
3、执行更新测试操作:
@Test
public void testUpdate(){
User user = new User();
user.setId(6L);
user.setName("mofan");
user.setAge(19);
int result = userMapper.updateById(user);
System.out.println(result);
System.out.println(user);
}
4、查看结果:

代码级别
1、由于前面的测试,我们需要移除数据库的默认值和更新操作:
2、实体类的属性上增加注解
// 字段填充内容
@TableField(fill = FieldFill.INSERT)
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
3、编写处理器来处理这些注解
@Slf4j
@Component // 处理器添加到容器中
public class MyMetaObjectHandler implements MetaObjectHandler {
// 插入的填充策略
@Override
public void insertFill(MetaObject metaObject) {
log.info("开始进行插入...");
// setFieldValByName(String fieldName, Object fieldVal, MetaObject metaObject)
this.setFieldValByName("createTime",new Date(),metaObject);
this.setFieldValByName("updateTime",new Date(),metaObject);
}
// 更新的填充策略
@Override
public void updateFill(MetaObject metaObject) {
log.info("开始进行更新...");
this.setFieldValByName("updateTime",new Date(),metaObject);
}
}
4、测试插入:
@Test
public void testInsert(){
User user = new User();
user.setName("mofan");
user.setAge(17);
user.setEmail("cy.mofan@qq.com");
int result = userMapper.insert(user); // 自动生成ID
System.out.println(result);
System.out.println(user);
}
5、观察插入数据:

4.5 乐观锁
与乐观锁相对的有:悲观锁
乐观锁:一个「乐观」的锁,它总是认为不会出问题,无论干什么都不会加锁。如果出了问题,再次更新值测试
悲观锁:一个「悲观」的锁,它总是认为会出现问题,无论干什么都会加上锁,然后再去操作!
在这主要讲解 乐观锁 机制。
乐观锁插件的适用场景
官方解释:乐观锁插件
意图:
当要更新一条记录的时候,希望这条记录没有被别人更新
乐观锁实现方式:
- 取出记录时,获取当前 version
- 更新时,带上这个 version
- 执行更新时,set version = newVersion where version = oldVersion
- 如果 version 不对,就更新失败
-- 假设有两个线程A、B同时修改一条记录
update user set name = "mofan", version = version + 1
where id = 6 and version = 1
-- 但是B抢先完成了修改,version修改成2
-- 这个时候A就不能修改数据了,实现线程通信的安全
测试 MP 的乐观锁插件
1、给数据库增加 version 字段,并设置默认值为 1
2、给实体类增加相应的字段,并添加注解 @Version
@Version
private Integer version;
3、注册组件
@EnableTransactionManagement // 默认已添加该注解
@Configuration
// 移动主配置文件下扫描mapper文件夹的注解(非必须,仅仅为了美观,与乐观锁配置无关)
@MapperScan("com.yang.mapper")
public class MyBatisPlusConfig {
// 注册乐观锁插件
@Bean
public OptimisticLockerInterceptor optimisticLockerInterceptor() {
return new OptimisticLockerInterceptor();
}
}
4、编写测试方法进行测试:
// 测试乐观锁成功
@Test
public void testOptimisticLocker(){
// 查询一个用户
User user = userMapper.selectById(1l);
// 修改信息
user.setName("mofan");
user.setEmail("cy.mofan@qq.com");
// 更新信息
userMapper.updateById(user);
}
数据库结果:

我们再来测试一下修改失败的情况,主要模拟多线程情况下线程插队的情况:
// 测试乐观锁失败 多线程下
@Test
public void testOptimisticLocker2(){
// 查询一个用户
User user = userMapper.selectById(1l);
// 修改信息
user.setName("mofan111");
user.setEmail("cy.mofan@qq.com");
// 模拟另一个线程插队操作
User user2 = userMapper.selectById(1l);
// 修改信息
user2.setName("mofan222");
user2.setEmail("cy.mofan@qq.com");
userMapper.updateById(user2);
// 自旋锁来尝试多次提交
userMapper.updateById(user); // 如果没乐观锁,就会覆盖插队线程值
}
如果没有乐观锁,最后一次的修改会覆盖前一次的修改,但是有了乐观锁就不会出现这种情况了。查看数据库修改结果:

查看数据库后,结果与我们预期一样。
4.6 数据查询
// 根据id查询单个用户
@Test
public void testSelectById(){
User user = userMapper.selectById(1l);
System.out.println(user);
}
// 根据id查询多个用户
@Test
public void testSelectBatchId(){
List<User> users = userMapper.selectBatchIds(Arrays.asList(1, 2, 3));
users.forEach(System.out::println);
}
// 条件查询之一 使用map进行操作
@Test
public void testSelectByBatchIds(){
HashMap<String, Object> map = new HashMap<>();
// 自定义查询
map.put("name","默烦");
map.put("age","18");
// 查询name = "默烦",age=18的所有用户
List<User> users = userMapper.selectByMap(map);
users.forEach(System.out::println);
}
4.7 数据分页查询
分页查询的方法有很多,比如:
1、原始的 limit 进行分页
2、第三方插件进行分页,比如:PageHelper
3、MP 内置的分页插件
使用 MP 内置的分页插件
1、配置拦截器组件
官方配置:
//Spring boot方式
@EnableTransactionManagement
@Configuration
// 指明需要被扫描的 Mapper 接口文件
@MapperScan("com.baomidou.cloud.service.*.mapper*")
public class MybatisPlusConfig {
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
// 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认false
// paginationInterceptor.setOverflow(false);
// 设置最大单页限制数量,默认 500 条,-1 不受限制
// paginationInterceptor.setLimit(500);
// 开启 count 的 join 优化,只针对部分 left join
paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true));
return paginationInterceptor;
}
}
我测试用的配置:
// 分页组件
@Bean
public PaginationInterceptor paginationInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
final PaginationInnerInterceptor innerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
// 单页分页条数限制
innerInterceptor.setMaxLimit(150L);
interceptor.addInnerInterceptor(innerInterceptor);
return interceptor;
}
2、直接使用 Page 对象即可
// 测试分页查询
@Test
public void testPage(){
// 当前页:2 页面数据量:5条
Page<User> page = new Page<>(2,5);
userMapper.selectPage(page, null);
System.out.println("总记录数为:"+page.getTotal());
page.getRecords().forEach(System.out::println);
}
控制台输出:

数据库数据对比:
4.8 数据删除
三种基本的删除:删除单个数据、删除多个数据、根据条件删除。
删除单个数据
// 删除单个数据
@Test
public void testDeleteById(){
userMapper.deleteById(1272430143247347718L);
}
删除数据前的数据库:参考分页查询最后一张数据库示例图。
删除数据后的数据库:

删除多个数据
// 删除多个数据
@Test
public void testDeleteBatchById(){
userMapper.deleteBatchIds(Arrays.asList(1272430143247347715L,1272430143247347716L));
}
删除数据后的数据库:

根据条件删除
// 根据条件删除
@Test
public void testDeleteMap(){
HashMap<String, Object> map = new HashMap<>();
map.put("name","默烦");
userMapper.deleteByMap(map);
}
删除数据后的数据库:

4.9 逻辑删除
物理删除:从数据库中直接移除数据。
逻辑删除:数据没有从数据库中移除,只是打了个标志让用户看不到。(学过 HBase 可以参考 HBase 的删除逻辑)
应用场景:管理员可以查看被删除的数据。
测试逻辑删除
1、在数据表中增加逻辑删除字段 delete,默认值为 0,表示未被删除。
如果在数据库级别没有设置默认值,那么需要添加自动填充,如:
@Override
public void insertFill(MetaObject metaObject) {
...
this.setFieldValByName("deleted", 0, metaObject);
}
2、实体类中增加属性,并添加注解 @TableLogic
// 自动填充时使用
// @TableField(fill = FieldFill.INSERT, value = "deleted")
@TableLogic
private Integer deleted;
3、编写配置文件
mybatis-plus:
global-config:
# 配置逻辑删除
db-config:
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
4、进行测试
@Test
public void testDeleteById(){
userMapper.deleteById(1L);
}
控制台输出信息:

数据表信息:

我们在进行查询,看看能够查询到被逻辑删除的数据:
@Test
public void testSelectById(){
User user = userMapper.selectById(1l);
System.out.println(user);
}
控制台打印信息:

我们发现,引进逻辑删除后再进行查询会自动过滤被逻辑删除的数据。
5. 条件构造器
官方参考文档:条件构造器
5.1 QueryWrapper
我们写一些复杂的 SQL 时,就可以用它来替代!
前置:创建一个测试类 WrapperTest:
@SpringBootTest
public class WrapperTest {
@Autowired
private UserMapper userMapper;
// ...
}
当前数据表数据:

查询 name 不为空、email 不为空、年龄大于等于 12 的用户
@Test
void contextLoads() {
// 查询name不为空、email不为空、年龄大于等于12的用户
// 与map对比学习
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.isNotNull("name")
.isNotNull("email")
.ge("age",12);
userMapper.selectList(wrapper).forEach(System.out::println);
}
查询 name 是默烦的 一个 用户
@Test
void test2(){
// 查询name是默烦的用户
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("name","默烦");
// 查询一个数据
User user = userMapper.selectOne(wrapper);
System.out.println(user);
}
查询 age 范围是 20 ~ 30 的用户
@Test
void test3(){
// 查询age范围是 20 ~ 30 的用户
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.between("age",20,30);
Integer count = userMapper.selectCount(wrapper);
System.out.println("用户数量为"+count);
}

模糊查询
// 模糊查询
@Test
void test4(){
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.notLike("name","e") // name中不包含e的用户
.likeRight("email","t"); // email以t开头的用户
List<Map<String, Object>> maps = userMapper.selectMaps(wrapper);
maps.forEach(System.out::println);
}
嵌套查询
@Test
void test5(){
QueryWrapper<User> wrapper = new QueryWrapper<>();
// id 在子查询出来
wrapper.inSql("id","select id from user where id < 3");
List<Object> objects = userMapper.selectObjs(wrapper);
objects.forEach(System.out::println);
}
通过 id 进行降序排序
@Test
void test6(){
QueryWrapper<User> wrapper = new QueryWrapper<>();
// 通过id进行降序排序
wrapper.orderByDesc("id");
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}
5.2 LambdaQueryWrapper
在使用 QueryWrapper 时查询字段都是魔法值,都是由开发人员编写的列名。
如果需要重构代码,使用这种方式将十分麻烦,因此在 MP 中有 LambdaQueryWrapper 来解决这个问题。
比如,现将下述代码进行改写:
@Test
void contextLoads() {
// 查询name不为空、email不为空、年龄大于等于12的用户
// 与map对比学习
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.isNotNull("name")
.isNotNull("email")
.ge("age",12);
userMapper.selectList(wrapper).forEach(System.out::println);
}
改写后的代码:
@Test
public void testLambdaQueryWrapper() {
LambdaQueryWrapper<User> wrapper = Wrappers.<User>lambdaQuery().isNotNull(User::getName)
.isNotNull(User::getEmail)
.ge(User::getAge, 12);
userMapper.selectList(wrapper).forEach(System.out::println);
}
对于 QueryWrapper 对象的获取采用 new 关键字,而 LambdaQueryWrapper 对象的获取则习惯采用 Wrappers 的静态方法获取。
注意: 在使用 MP 的条件构造器时,请注意 and() 方法的使用规则,并区分与 or() 方法的区别。
需要注意的是,在低版本的 MP 中,可能并没有 Wrappers 类(请以参考文档为准)。
6. 代码生成器
官方参考文档:代码生成器
首先导入依赖:
<!-- mybatis-plus -->
<!--3.0.7版本移除 对 mybatis-plus-generator 包的依赖,自己按需引入,尽在还需要导入模板依赖-->
<!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-generator -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>Latest Version</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.0</version>
</dependency>
编写生成器类:
package com.yang;
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 javax.xml.stream.FactoryConfigurationError;
import java.util.ArrayList;
/**
* @author 默烦
* @date 2020/6/16
*/
// 代码自动生成器
public class MofanCode {
public static void main(String[] args) {
// 构建一个 代码自动生成器 对象
AutoGenerator mpg = new AutoGenerator();
/************配置策略***********/
// 1. 全局配置
// 注意:包别导错了
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dor"); // 获取当前系统目录
gc.setOutputDir(projectPath+"/src/main/java"); // 代码输出目录
gc.setAuthor("默烦");
gc.setOpen(false); // 是否打开资源文件夹
gc.setFileOverride(false); // 是否覆盖原有文件
gc.setServiceName("%sService"); // 去默认Service的I前缀
gc.setIdType(IdType.ID_WORKER); // 主键生成策略
gc.setDateType(DateType.ONLY_DATE); // 日期类型
gc.setSwagger2(true); // 自动配置Swagger文档
mpg.setGlobalConfig(gc);
// 2. 设置数据源
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://localhost:3306/mybatis_plus?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.yang");
pc.setEntity("pojo");
pc.setMapper("mapper");
pc.setService("service");
pc.setController("controller");
mpg.setPackageInfo(pc);
// 4. 策略配置
StrategyConfig strategy = new StrategyConfig();
/*************重点修改************/
strategy.setInclude("user","tags"); // 设置映射的表名
/*************重点修改************/
strategy.setNaming(NamingStrategy.underline_to_camel);
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
strategy.setEntityLombokModel(true); // 自动生成lombok
strategy.setLogicDeleteFieldName("deleted"); // 设置逻辑删除字段
// 自动填充策略
TableFill create_time = new TableFill("create_time", FieldFill.INSERT);
TableFill update_time = new TableFill("update_time", FieldFill.INSERT_UPDATE);
ArrayList<TableFill> tableFills = new ArrayList<>();
tableFills.add(create_time);
tableFills.add(update_time);
strategy.setTableFillList(tableFills);
// 乐观锁设置
strategy.setVersionFieldName("version");
strategy.setRestControllerStyle(true);
// url 设置 localhost:8080/hello_mofan_1
strategy.setControllerMappingHyphenStyle(true);
mpg.setStrategy(strategy);
mpg.execute(); // 执行
}
}
以上代码按要求修改即可使用。
7. 拓展批量处理
7.1 批量插入
注意: 拓展的批量插入只适用于 MySQL 数据库。
在 MyBatis-Plus 中存在现成的批量插入方法,我们只需进行简单的拓展即可使用:
1、自定义 Injector 并继承 DefaultSqlInjector 类;
2、重写 getMethodList() 方法,添加现成的批量插入类,并注入 MyBatis-Plus 配置类中;
3、最后拓展通用 Mapper 即可。
自定义 Injector 并重写 getMethodList() 方法:
/**
* @author mofan
* @date 2021/3/14 21:54
* 支持自定义数据方法注入
*/
public class EasySqlInjector extends DefaultSqlInjector {
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
// 防止父类的方法不可使用
List<AbstractMethod> methodList = super.getMethodList(mapperClass);
methodList.add(new InsertBatchSomeColumn());
return methodList;
}
}
在 MyBatis-Plus 配置类中注入自定义 Injector:
@Bean
public EasySqlInjector easySqlInjector() {
return new EasySqlInjector();
}
拓展通用 Mapper,后续的 Mapper 接口不再继承 BaseMapper,而是继承拓展的通用 Mapper。
/**
* @author mofan
* @date 2021/3/14 21:57
* 拓展通用 Mapper,支持数据批量插入
*/
public interface MybatisBaseMapper<T> extends BaseMapper<T> {
/**
* 批量插入 仅适用于 MySQL
* @param entityList 实体列表
* @return 插入的行数
*/
int insertBatchSomeColumn(List<T> entityList);
}
使用方法举例:
@Mapper
@Repository
public interface UserInfoDao extends MybatisBaseMapper<UserInfo> {}
7.2 批量更新
相较于简单的批量插入拓展,在拓展批量更新时需要我们自己编写批量更新模板方法,然后其他步骤就和批量插入的拓展类似。
首先在数据库连接上添加 allowMultiQueries=true 属性:
datasource:
username: xxx
password: xxx
# 项目中已拓展 Mybatis-plus 的批量更新,需要在连接信息中添加 allowMultiQueries=true 表示开启批量更新
url: jdbc:mysql:///xxx?serverTimezone=GMT%2b8&useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true&allowMultiQueries=true
driver-class-name: com.mysql.cj.jdbc.Driver
批量更新模板方法:
/**
* @author mofan
* @date 2021/4/14 15:38
* 批量更新方法实现,条件为主键,选择性更新
*/
@Slf4j
public class UpdateBatchMethod extends AbstractMethod{
private static final long serialVersionUID = 1147163282820238330L;
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
String sql = "<script>\n<foreach collection=\"list\" item=\"item\" separator=\";\">\nupdate %s %s where %s=#{%s} %s\n</foreach>\n</script>";
String additional = tableInfo.isWithVersion() ? tableInfo.getVersionFieldInfo().getVersionOli("item", "item.") : "" + tableInfo.getLogicDeleteSql(true, true);
String setSql = sqlSet(tableInfo.isWithLogicDelete(), false, tableInfo, false, "item", "item.");
String sqlResult = String.format(sql, tableInfo.getTableName(), setSql, tableInfo.getKeyColumn(), "item." + tableInfo.getKeyProperty(), additional);
// log.debug("sqlResult----->{}", sqlResult);
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sqlResult, modelClass);
// 第三个参数必须和MybatisBaseMapper的自定义方法名一致
return this.addUpdateMappedStatement(mapperClass, modelClass, "updateBatch", sqlSource);
}
}
在自定义的 Injector 添加拓展的批量更新模板类:
public class EasySqlInjector extends DefaultSqlInjector {
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
// 防止父类的方法不可使用
List<AbstractMethod> methodList = super.getMethodList(mapperClass);
methodList.add(new InsertBatchSomeColumn());
methodList.add(new UpdateBatchMethod());
return methodList;
}
}
最后拓展通用 Mapper:
public interface MybatisBaseMapper<T> extends BaseMapper<T> {
/**
* 批量插入 仅适用于 MySQL
* @param entityList 实体列表
* @return 插入的行数
*/
int insertBatchSomeColumn(List<T> entityList);
/**
* 自定义批量更新,条件为主键
* @param entityList 实体列表
* @return 是否成功
*/
int updateBatch(@Param("list") List<T> entityList);
}
批量更新使用注意事项
1、仅测试过 MySQL 数据库,对于其他数据库的支持尚不明确;
2、无法做到批量更新字段值为 NULL,针对这种情况请自行编写 XML,比如:
<!-- 重置当年考试科目信息 (将 campus、classroomName 列设置为 null,used 列设置为 false) -->
<update id="resetExamSubject">
<foreach collection="list" item="item" index="index" close="" open="" separator=";">
UPDATE tb_exam_subject
<set>
examination_location = NULL,
used = false
</set>
WHERE id = ${item.id} AND is_deleted = 0
</foreach>
</update>
8. 通用枚举
在 JDK 1.5 之后引入了枚举,如果要在 MP 中实现通用枚举也很简单,只需要配置枚举类的路径,然后使用注解即可。
为了规范枚举的使用,习惯让自定义枚举继承一个接口:
/**
* @author mofan
* 枚举信息,一般来说,每个枚举类都应该实现这个接口
*/
public interface EnumInfo extends Serializable {
/**
* 获取枚举值对应的代码
* @return 对应的代码
*/
String code();
/**
* 获取枚举值对应的信息
* @return 对应的信息
*/
String message();
}
假设有这样一个枚举:
/**
* @author mofan
* 专业类型
*/
public enum MajorTypeEnum implements EnumInfo {
/**
* 学术型
*/
ACADEMIC("Academic", "学术型"),
/**
* 专业学位
*/
PROFESSIONAL_DEGREE("ProfessionalDegree", "专业学位");
private String code;
private String message;
MajorTypeEnum(String code, String message) {
this.code = code;
this.message = message;
}
@Override
public String code() {
return this.code;
}
@Override
public String message() {
return this.message;
}
}
这个枚举类所在的包路径为:indi.mofan.common.enums。
现在我想让这个枚举保存到数据库中的值是 code,序列化后得到的是 message。那么首先需要在配置文件中指定枚举所在的包路径:
3.5.2 版本开始,无需设置扫描的枚举包!
mybatis-plus:
# 设置枚举包扫描。3.5.2 版本开始,省略此配置。
type-enums-package: indi.mofan.common.enums
然后在 code 属性上使用 @EnumValue 注解:
@EnumValue
private String code;
在 message 上使用 @JsonValue 注解即可:
@JsonValue
private String message;
9. 常用配置文件
mybatis-plus:
# 映射文件位置
mapper-locations: classpath:indi/mofan/mybatis/*.xml
configuration:
# 映射驼峰(实体属性名)到下划线(列名)
map-underscore-to-camel-case: true
# MyBatis-Plus配置日志
# 使用默认控制台输出,不需要导入依赖
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
# 配置逻辑删除
db-config:
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
# 设置实体类所对应的表的统一前缀
table-prefix: tb_
# 设置统一的主键生成策略
id-type: assign_id
# 设置枚举包扫描。3.5.2 版本开始,省略此配置。
type-enums-package: indi.mofan.common.enums
注意: 在 SpringBoot 项目中,我们常把 MyBatis 的 SQL 映射文件放在 resources 目录下。