MybatisPlus 的一些基本使用

本是想跟着尚医通这个项目来做个完整的项目,结果视频的结构把我整麻了。进行一个跑路。但拜其所赐,对 MP 进行了一些学习。

MyBatis-Plus(下面简称 MP)是对 MyBatis 的增强和扩展,它不改变 MyBatis 的 API,但是提供了更多强大的功能以帮助编码。

对 MP 最基础的使用非常简单——让 Mapper 接口继承一个所谓的BaseMapper接口即可,这个BaseMapper是泛型接口,其类型为操作的实体(当然,对应数据库的一张表)。

在官方示例中,使用MapperScan注解对 Mapper 进行扫描和注入,但我认为使用Mapper注解语义上更为清晰,且 IDE 支持友好(我这 IDEA 不能识别 MapperScan 所导入的 Bean,不知道为何)。

1
2
3
4
5
6
7
8
9
10
11
// entity/User.java
@Data
public class User {
@TableId private Long id;
private String name;
private Integer age;
private String email;
}

// mapper/UserMapper.java
public interface UserMapper extends BaseMapper<User> {}

简单 CRUD

BaseMapper中定义了许多 CRUD 方法,能够满足大多数业务需求。许多方法可以通过传入所谓的Wrapper对象给定查询/更改条件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
userMapper.selectById(2); // 根据主键进行查询
/*
结果:
User(id=2, name=Jack, age=20, email=test2@baomidou.com)
*/
userMapper.selectList(null); // 整表查询
/*
结果:
User(id=1, name=Jone, age=18, email=test1@baomidou.com)
User(id=2, name=Jack, age=20, email=test2@baomidou.com)
...
*/
userMapper.selectOne(new QueryWrapper<User>().eq("name", "Tom")); // 带条件的查询
/*
结果:
User(id=3, name=Tom, age=28, email=test3@baomidou.com)
*/

selectBatchIds方法接受一个主键 Id 列表并返回结果集合,但或许查询多条记录时最实用的方法是selectByMap,其接受一个Map,k 为字段名,v 为值,返回 List,这种形式和 where 后接一连串相等运算符的形式基本等效,更复杂的查询得去使用QueryWrapper了。

1
2
3
4
5
6
7
userMapper.selectBatchIds(Arrays.asList(1, 2, 3,));
// [User(id=1, name=Jone, age=18, email=test1@baomidou.com, createTime=null, updateTime=null), User(id=2, name=Jack, age=20, email=test2@baomidou.com, createTime=null, updateTime=null), User(id=3, name=Tom, age=28, email=test3@baomidou.com, createTime=null, updateTime=null)]

Map<String, Object> selecter = new HashMap<>();
selecter.put("age", 28);
userMapper.selectByMap(selecter);
// [User(id=3, name=Tom, age=28, email=test3@baomidou.com, createTime=null, updateTime=null)]

关于添加操作,MP 提供了方法,使能够通过实体的实例进行添加。

1
2
3
4
5
6
7
User newUser = User.builder()
.id(10L)
.age(20)
.email("abcd@163.com")
.name("Ri")
.build();
userMapper.insert(newUser); // insert 方法直接接受一个实体(在这里是 User)的实例

可以配置如下项让 MP 输出日志到标准输出流——

1
2
3
4
5
6
7
8
9
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 相关输出如下:
# ==> Preparing: SELECT id,name,age,email FROM user WHERE id=?
# ==> Parameters: 2(Integer)
# <== Columns: id, name, age, email
# <== Row: 2, Jack, 20, test2@baomidou.com
# <== Total: 1

关于 MP 提供的UpdateById方法,该方法接受一个实体——其应当设定 Id 以及将要修改的域(其它的设为空),这个方法将会根据该实体的 ID 和非 null 的属性自动生成对应 SQL 语句,这种方法非常方便,特别是结合建造者模式!比如下面的实例——

1
2
3
4
5
6
7
8
9
10
11
User user = User.builder()
.id(1L)
.name("omgwtf")
.age(1020)
.build(); // 这里 email 为 null,没有设定
userMapper.updateById(user);
/*
相关日志:
==> Preparing: UPDATE user SET name=?, age=? WHERE id=?
==> Parameters: omgwtf(String), 1020(Integer), 1(Long)
*/

关于删除操作,MP 提供了deletedeleteByIddeleteBatchIdsdeleteByMap方法,其分别接受 Wrapper,主键 Id,Id 集合,哈希表,满足了大多数操作需要。

Wrapper

Wrapper 是 MP 提供的用以生成相应 SQL 语句的对象,其对 SQL 的大部分(所有?)运算符进行了抽象和方法化,从而能够让用户能够不写一行 SQL 代码而对数据库进行操作。

就操作对象来说,Wrapper 分为 QueryWrapper 和 UpdateWrapper,顾名思义,其分别负责查询和更新/删除。

就操作形式来说,Wrapper 分为普通 Wrapper 和 LambdaWrapper,普通 Wrapper 在操作中一般接受 Map,LambdaWrapper 一般接受 Lambda 表达式——这显然是更加方便易读的。

Wrapper 通过链式调用进行使用。各个表达式默认通过 and 进行连接。但也可以显式指定为 or。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
new QueryWrapper<User>()
.eq("name", "Jone")
.eq("age", 18); // 可以使用 allEq
// 使用 selectList 方法,生成 SQL 形如 SELECT ... FROM user WHERE (name = ? AND age = ?)

new QueryWrapper<User>()
.eq("name", "Jone")
.eq("age", 18)
.or()
.eq("name", "Jack")
.eq("age", 200);
// SELECT ... FROM user WHERE (name = ? AND age = ? OR name = ? AND age = ?)

// 嵌套子句。这里的 or 和 and 方法的作用形式就像中缀运算符
new QueryWrapper<User>()
.or(i->{
i.eq("name", "Jone");
i.eq("age", 18);
})
.or(i->{
i.eq("name", "Jack");
i.eq("age", 200);
});
// SELECT ... FROM user WHERE ((name = ? AND age = ?) OR (name = ? AND age = ?))

自动填充

MP 还提供了自动填充的功能——在进行插入的时候,能自定义某些字段的填充。这种填充我在毕设项目时也进行过使用,当时是通过触发器进行实现的。

自动填充分为添加时设置值以及修改时设置值,其通过INSERTDEFAULT(默认不处理),INSERT_UPDATEUPDATE这四个枚举指定。要使用自动填充需要进行两个操作——在字段上添加相关注解;定义自动填充处理器。下面展示了其定义和应用。

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
// 实体类
@Data
@Builder
public class User {
// ...
// TableField 注解默认会把实体类的小驼峰命名法转换成下划线命名法并作为数据库表中相应字段,但是也可以直接显式指定相应的表字段名。
@TableField(value = "create_time", fill = FieldFill.INSERT)
private Date createTime; // 创建时间,在插入时进行赋值
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime; // 更新时间,在更新时进行赋值
}

// 处理器,处理器置于 handler 包下而非 config 包下,因为其并非负责配置,而是负责业务
// 为什么处理器不需要与实体类绑定?
@Component
public class UserMapperHandler implements MetaObjectHandler {
// 插入操作时执行
@Override
public void insertFill(MetaObject metaObject) {
// 设置实体类实例的 createTime 字段为当前时间
this.setFieldValByName("createTime", new Date(), metaObject);
this.setFieldValByName("updateTime", new Date(), metaObject);
}
// 修改操作时执行
@Override
public void updateFill(MetaObject metaObject) {
this.setFieldValByName("updateTime", new Date(), metaObject);
}
}

/*
插入时的日志,可以看到插入时 createTime 字段和 updateTime 字段都生成在 SQL 中
==> Preparing: INSERT INTO user ( id, name, age, email, create_time, update_time ) VALUES ( ?, ?, ?, ?, ?, ? )
==> Parameters: 11(Long), Rim(String), 20(Integer), abcdefg@163.com(String), 2021-07-28 18:21:42.919(Timestamp), 2021-07-28 18:21:42.919(Timestamp)
<== Updates: 1

更新时的日志,可以看到插入时 updateTime 字段生成在 SQL 中
==> Preparing: UPDATE user SET name=?, age=?, update_time=? WHERE id=?
==> Parameters: omgwtf(String), 1020(Integer), 2021-07-28 18:29:01.452(Timestamp), 1(Long)
<== Updates: 1
*/

可以认为自动填充做了这样的工作——进行操作时,检查所有字段,查找所有带 TableField 注解且定义了 fill 属性的字段,并将符合条件的字段插入到 SQL 中(就像非 null 的字段),因此这些字段应当(不是必须)在处理器中进行赋值。

一个有趣的地方是,如果定义了多个处理器,Spring Boot 会提示注入失败——有多个可选 Bean,看来应当在配置文件中进行相关定义,而不是直接给处理器进行 Component 注解。

分页

分页在实践中可能是一个比较重要的话题,虽然我从来没写过,都是一口气全部拿到的 w。

MP 提供了分页查询的功能,其是以插件的形式实现的,具体来说是自定义相应的 bean。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 用的还是上面的 MyBatisPlusConfig
@Configuration
@MapperScan("me.ykn.mapper") // 还是使用 Mapper 注解语义更加清晰一些
public class MyBatisPlusConfig {
// 自动分页插件
@Bean
public PaginationInnerInterceptor paginationInnerInterceptor() {
return new PaginationInnerInterceptor(DbType.MYSQL);
}
// 插件主体
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(paginationInnerInterceptor()); // 添加分页插件,其它插件也通过这种形式进行添加
return interceptor;
}
}

分页查询的应用是简单的,许多方法提供了对应返回 Page 的方法。

1
2
3
4
Page<User> page = new Page<>(1, 2); // 页数,条数,页数从 1 开始(!)
Page<User> res = userMapper.selectPage(page, null); // 第二个参数是 Wrapper
System.out.println(res.getRecords()); // getRecords 方法返回相应 List
// Page 类还提供了更多方便的方法,足以支撑日常使用

就这些吧。


本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 协议 ,转载请注明出处!