Mybatis-Plus

前言:
    根据狂神视频整理
    为什么要学它,因为mybatisplus可以节省我们大量的工作时间,所有的CRUD代码他都可以自动完成,所以才说是懒人后端的福音😁
    这个是中国人开发的,所以官方写得比较好理解,官方配置文档地址点点我

1. 简介

MyBatis-Plus (简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。

2. 快速入门

  1. 对应的数据库脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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)
);

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');
  1. 在pom里导入mp(这里是比较早的版本,推荐使用较新的版本
1
2
3
4
5
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
不要同时导入mybatis 和mp,可能会引起版本冲突
  1. 数据库配置
1
2
3
4
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.url=jdbc:mysql://localhost:3306/dbtest?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
这里的时区设置的是东八区
  1. 写实体类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import com.baomidou.mybatisplus.annotation.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

/**
* @program: mybatis-plus-demo1
* @description:
* @author: Mr.Like
* @create: 2022-05-19 16:53
**/
@Data
@AllArgsConstructor
@NoArgsConstructor

public class User {
private Long id;
private String name;
private Integer age;
private String email;
}
  1. 编写mapper
1
2
3
4
5
@Mapper
public interface UserMapper extends BaseMapper<User>{
}

// 是的,你没有看错,就这么点
  1. 在springboot的启动类中添加扫描注解
1
2
3
4
5
6
7
8
9
@SpringBootApplication
@MapperScan("com.mercury.mybatisplusdemo1.mapper") //mapper层的地址
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}

}
  1. 测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@SpringBootTest
class MybatisPlusDemo1ApplicationTests {


@Autowired
private UserMapper userMapper;


@Test
void contextLoads() {
List<User> users = userMapper.selectList(null);
users.forEach(System.out::println);
//comsumer?
}
}
  1. 输出
1
2
3
4
5
User(id=1, name=Jone, age=18, email=test1@baomidou.com)
User(id=2, name=Jack, age=20, email=test2@baomidou.com)
User(id=3, name=Tom, age=28, email=test3@baomidou.com)
User(id=4, name=Sandy, age=21, email=test4@baomidou.com)
User(id=5, name=Billie, age=24, email=test5@baomidou.com)

这里配置一下日志信息方便查看

1
2
#配置日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4b65d9f4] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@1702089463 wrapping com.mysql.cj.jdbc.ConnectionImpl@4052c8c2] will not be managed by Spring
==> Preparing: SELECT id,name,age,email FROM user //可以看到自动生成的sql
==> Parameters:
<== Columns: id, name, age, email
<== Row: 1, Jone, 18, test1@baomidou.com
<== Row: 2, Jack, 20, test2@baomidou.com
<== Row: 3, Tom, 28, test3@baomidou.com
<== Row: 4, Sandy, 21, test4@baomidou.com
<== Row: 5, Billie, 24, test5@baomidou.com
<== Total: 5
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4b65d9f4]
User(id=1, name=Jone, age=18, email=test1@baomidou.com)
User(id=2, name=Jack, age=20, email=test2@baomidou.com)
User(id=3, name=Tom, age=28, email=test3@baomidou.com)
User(id=4, name=Sandy, age=21, email=test4@baomidou.com)
User(id=5, name=Billie, age=24, email=test5@baomidou.com)
2022-05-19 17:19:55.314 INFO 23644 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated...
2022-05-19 17:19:55.324 INFO 23644 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed.

3. 主键生成策略

1
2
3
4
5
6
7
8
9
10
11
//测试插入
@Test
public void insert(){
User user = new User();
user.setName("mercury02");
user.setAge(3);
user.setEmail("william@163.com");
int insert = userMapper.insert(user);//自动生成id
System.out.println(insert);
System.out.println(user);
}

插入结果

1
2
3
==> Preparing: INSERT INTO user ( id, name, age, email ) VALUES ( ?, ?, ?, ? ) 
==> Parameters: 1527226339787800577(Long), mercury(String), 3(Integer), chenning_william@163.com(String)
<== Updates: 1

这里默认使用的是分布式系统唯一id生成 雪花算法(SnowFlake)

雪花算法的由来:

  • Twitter使用scala语言开源了一种分布式 id 生成算法——SnowFlake算法,被翻译成了雪花算法。
  • 因为自然界中并不存在两片完全一样的雪花的,每一片雪花都拥有自己漂亮独特的形状、独一无二。雪花算法也表示生成的ID如雪花般独一无二。

3.1 主键自增

配置主键自增

实体类的字段上

1
2
3
4
5
6
7
8
public class User {

@TableId(type = IdType.AUTO)
private Long id;
private String name;
private Integer age;
private String email;
}

同时数据库里面也要设置为主键自增

can can 源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public enum IdType {
AUTO(0), //数据库id自增
NONE(1), //未设置主键
INPUT(2), //手动输入
ID_WORKER(3), //默认的全局唯一id(雪花
UUID(4), //全局唯一id
ID_WORKER_STR(5);//ID_WORKER 的字符串表示法

private int key;

private IdType(int key) {
this.key = key;
}

public int getKey() {
return this.key;
}
}

4. 更新操作

1
2
3
4
5
6
7
@Test
public void update(){
User user = new User();
user.setId(5l);
user.setName("william");
userMapper.updateById(user); //注意里面的参数是一个对象
}
1
2
3
==> Preparing: UPDATE user SET name=? WHERE id=? 
==> Parameters: william(String), 5(Long)
<== Updates: 1

5. 自动填充

创建时间,修改时间,这些操作都是自动化完成的

在国际标准中所有的数据库表gmt_create gmt_modified几乎所有的表都要配置上,而且需要自动化

5.1 数据库级别的修改

增加create_time update_time字段

可以看到默认的时间

5.2 代码级别的修改

首先还原数据库的设置,将默认CURRENT_TIMESTAMP和更新取消掉

  1. 实体类字段属性上添加注解
1
2
3
4
5
@TableField(fill = FieldFill.INSERT)
private Date createTime;

@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
  1. 编写handler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
log.info("start insert fill ...");
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); //后面更新的时候就只需要填充update这个时间就行
}
}

6. 乐观锁配置

乐观锁:总认为不会出现问题,无论干什么都不会去上锁,如果出现了问题,再次更新值测试

悲观锁:它总认为总是出现问题,无论干什么都会去上锁,然后去操作

什么时候需要乐观锁呢?当要更新一条记录的时候,希望这条记录没有被别人更新

乐观锁实现方式:

  • 取出记录时,获取当前version
  • 更新时,带上这个version
  • 执行更新时, set version = newVersion where version = oldVersion(整数类型下 newVersion = oldVersion + 1)
  • 如果version不对,就更新失败
  1. 数据库里增加version字段 (默认初版为1)

image-20220522130711039

  1. 实体类中增加
1
2
@Version//乐观锁的注解
private Integer version;
  1. 注册组件

写个config来统一配置,新版的注册组件有所不同,详情看官方的配置文档

1
2
3
4
5
6
7
8
9
10
11
@MapperScan("com.mercury.mybatisplusdemo1.mapper") //可以把扫描mapper层的组件搬到这里
@EnableTransactionManagement //处理事务的注解
@Configuration
public class MybatisPlusConfig {

//乐观锁插件
@Bean
public OptimisticLockerInterceptor optimisticLockerInterceptor() {
return new OptimisticLockerInterceptor();
}
}
  1. 测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void testOptimisticLocker(){

User user1 = userMapper.selectById(1l);
user1.setName("mercury");
user1.setEmail("mercury@163.com");

//模拟还没完成修改的时候,突然来了个线程插队,提前完成了修改
User user2 = userMapper.selectById(1l);
user2.setName("william");
user2.setEmail("william@163.com");
userMapper.updateById(user2);


userMapper.updateById(user1);//version不对导致写入失败,如果没有乐观锁就会导致覆盖

}

7. 查询操作

7.1 单个和多个查询

1
2
3
4
5
6
7
8
9
10
@Test
public void testSelectById(){
//查询单个
User user = userMapper.selectById(1l);
System.out.println(user);

//查询多个
List<User> users = userMapper.selectBatchIds(Arrays.asList(1, 2, 3));
users.forEach(System.out::println);
}

7.2 条件查询

使用map实现

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void testSelectBycondition(){
//根据条件进行查询

HashMap<String, Object> Map = new HashMap<>();
//自定义条件查询
Map.put("name","william");
Map.put("age",18);
//查询名字为william并且年龄为18的数据
List<User> users = userMapper.selectByMap(Map);
users.forEach(System.out::println);
}
1
2
3
4
5
==>  Preparing: SELECT id,name,age,email,version,deleted,create_time,update_time FROM user WHERE name = ? AND age = ?
==> Parameters: william(String), 18(Integer)
<== Columns: id, name, age, email, version, deleted, create_time, update_time
<== Row: 5, william, 18, test5@baomidou.com, 1, 0, 2022-05-21 13:15:42, 2022-05-22 13:00:53
<== Total: 1

7.3 分页查询

分页查询的几种实现方式

  • 原始的limit进行分页

  • pageHelper第三方插件

  • MP分页

  1. 配置分页查询的组件(配置在config里)
1
2
3
4
5
6
7
8
9
10
11
12
//分页插件
@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;
}
  1. 然后直接使用page对象即可
1
2
3
4
5
6
7
8
9
10
@Test
public void testpage(){
//分页查询
//当前页,显示的数据数量
Page<User> page = new Page<>(2,5); //第二页的数据,每页5个
userMapper.selectPage(page,null);

page.getRecords().forEach(System.out::println);
System.out.println(page.getTotal());
}
1
2
3
4
5
6
7
8
9
10
==>  Preparing: SELECT COUNT(1) FROM user 
==> Parameters:
<== Columns: COUNT(1)
<== Row: 7 //一共7条数据
==> Preparing: SELECT id,name,age,email,version,create_time,update_time FROM user WHERE LIMIT 5,5
==> Parameters:
<== Columns: id, name, age, email, version, create_time, update_time
<== Row: 1527903812435374084, mercury03, 3, william@163.com, 1, 2022-05-22 13:27:46, 2022-05-22 13:27:46
<== Row: 1527903812435374085, mercury04, 3, mercury@163.com, 1, 2022-05-22 13:28:11, 2022-05-22 13:28:11
<== Total: 2 //第二页两条数据

8. 删除操作

8.1 单个和批量以及条件删除

1
2
3
4
5
6
7
8
    @Test
public void testdelete(){
userMapper.deleteById(1l);
//userMapper.deleteBatchIds(); 批量删除
//userMapper.deleteByMap(); 条件删除
}

//和上面的查询是一样的

8.2 逻辑删除

  • 物理删除 从数据库中直接移除

  • 逻辑删除 在数据库中没有被删除,而是通过一个变量来让他失效(类似于回收站

  1. 数据库增加deleted字段

image-20220522143025600

  1. 实体类中增加注解
1
2
@TableLogic
private Integer deleted;
  1. 增加配置
1
2
3
4
5
@Bean
public ISqlInjector sqlInjector(){
return new LogicSqlInjector();
}
//高版本的已经不需要配置这个,具体请参考官方的配置文件
  1. application配置
1
2
3
4
mybatis-plus.global-config.db-config.logic-delete-value= 1 
# 逻辑已删除值(默认为 1)
mybatis-plus.global-config.db-config.logic-not-delete-value= 0
# 逻辑未删除值(默认为 0)
  1. 测试,运行删除id为1的数据
1
2
3
==> Preparing: UPDATE user SET deleted=1 WHERE id=? AND deleted=0 // 可以看出这里虽然是删除,但其实走的是更新
==> Parameters: 1(Long)
<== Updates: 1

那既然没有实际的删除,那么查询的时候可以查询出来吗?

1
2
3
4
//查询的时候会自动过滤被删的数据
==> Preparing: SELECT id,name,age,email,version,deleted,create_time,update_time FROM user WHERE id=? AND deleted=0 //注意这个sql
==> Parameters: 1(Long)
<== Total: 0

9. 性能分析插件

我们在平时的开发中会遇到一些慢sql ,MP也提供性能分析插件,如果超过这个时间就会停止,狂神视频所讲的插件已经移除,官网现在推荐使用p6spy,具体的配置参考官方文档

10. 条件构造器

前面的所讲的都是一些简单查询,那么我们想实现相对较复杂的查询的时候那么该怎么操作呢,这个时候我们就可以使用mp提供的条件构造器了

下面六个示例基本够用了,如果不够,依然那句话,去查看官方文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
@Test
void contextLoadstest1(){
//使用条件构造
QueryWrapper<User> wrapper = new QueryWrapper<>();
//名字和邮箱不为空且年龄大于12的数据
wrapper
.isNotNull("name")
.isNotNull("email")
.ge("age",12);
userMapper.selectList(wrapper).forEach(System.out::println);
}

@Test
void contextLoadstest2(){
//使用条件构造
QueryWrapper<User> wrapper = new QueryWrapper<>();
//查询等于的
wrapper.eq("name","william");
System.out.println(userMapper.selectOne(wrapper));//查询一个唯一数据时用,不然会报错
}

@Test
void contextLoadstest3(){
//使用条件构造
QueryWrapper<User> wrapper = new QueryWrapper<>();
//查询年龄在20到30之间的
wrapper.between("age",20,30);
System.out.println(userMapper.selectCount(wrapper));//统计结果数
}

@Test
void contextLoadstest4(){
//使用条件构造
QueryWrapper<User> wrapper = new QueryWrapper<>();
//模糊查询
wrapper.notLike("name","e")
.likeRight("email","t");//t% 这里的left和right指的是%的位置
List<Map<String, Object>> maps = userMapper.selectMaps(wrapper);
maps.forEach(System.out::println);
}

@Test
void contextLoadstest5(){
//使用条件构造
QueryWrapper<User> wrapper = new QueryWrapper<>();

//子查询
wrapper.inSql("id","select id from user where id <3");
List<Object> objects = userMapper.selectObjs(wrapper);
objects.forEach(System.out::println);
}

@Test
void contextLoadstest6(){
//使用条件构造
QueryWrapper<User> wrapper = new QueryWrapper<>();

//根据id降序
wrapper.orderByDesc("id");

List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}

11. 代码生成器

这里是旧版的生成器,3.5.1+版本的生成器和旧版的生成器有很大的不同,并且不兼容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
//代码自动生成器,写在test里
public class CodeGenerator {

public static void main(String[] args) {
//我们需要构建一个代码生成器对象
AutoGenerator mpg = new AutoGenerator();
//怎么样去执行,配置策略
//1、全局配置
GlobalConfig gc = new GlobalConfig(); //注意这里导包的时候注意要导generator下面的那个,不然后面的点不出来
String projectPath = System.getProperty("user.dir");//获取当前目录
gc.setOutputDir(projectPath+"/src/main/java");//输出到哪个目录
gc.setAuthor("mercury");
gc.setOpen(false);
gc.setFileOverride(false);//是否覆盖
gc.setServiceName("%sService");//去Service的I前缀
gc.setIdType(IdType.ID_WORKER);
gc.setDateType(DateType.ONLY_DATE);
gc.setSwagger2(true);
mpg.setGlobalConfig(gc);

//2、设置数据源
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUsername("root");
dsc.setPassword("root");
dsc.setUrl("jdbc:mysql://localhost:3306/db?useSSL=false&serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8");
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setDbType(DbType.MYSQL);
mpg.setDataSource(dsc);


//3、包的配置
PackageConfig pc = new PackageConfig();
pc.setModuleName("study");
pc.setParent("com.wsk");
pc.setEntity("pojo");
pc.setMapper("mapper");
pc.setService("service");
pc.setController("controller");
mpg.setPackageInfo(pc);

//4、策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setInclude("admin","danyuan","building","room");//设置要映射的表名,只需改这里即可
strategy.setNaming(NamingStrategy.underline_to_camel);
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
strategy.setEntityLombokModel(true);//是否使用lombok开启注解
strategy.setLogicDeleteFieldName("deleted");
//自动填充配置
TableFill gmtCreate = new TableFill("gmt_create", FieldFill.INSERT);
TableFill gmtUpdate = new TableFill("gmt_update", FieldFill.INSERT_UPDATE);
ArrayList<TableFill> tableFills = new ArrayList<>();
tableFills.add(gmtCreate);
tableFills.add(gmtUpdate);
strategy.setTableFillList(tableFills);
//乐观锁配置
strategy.setVersionFieldName("version");
strategy.setRestControllerStyle(true);//开启驼峰命名
strategy.setControllerMappingHyphenStyle(true);//localhost:8080/hello_id_2
mpg.setStrategy(strategy);
mpg.execute();//执行
}
}

Mybatis-Plus
http://example.com/2022/05/20/Mybatis-Plus/
作者
Mercury
发布于
2022年5月20日
许可协议