MyBatis-Plus
MyBatisPlus简介
MyBatisPlus是MyBatis增强工具,在MyBatis基础上只做增强不做改变,为简化开发、提高效率而生。MyBatisPlus提供通用的mapper和service,可以在不编写任何SQL语句的情况下,快速实现对单表的CRUD、批量、逻辑删除、分页等操作。
HelloWorld
开发环境
IDE:idea 2021
JDK:JDK8+
构建工具:maven 3.5.4
MySQL版本:MySQL 8
Spring Boot:2.7.1
MyBatis-Plus:3.5.1
创建工程
安装插件
引入依赖
1 | <!-- mybatis-plus --> |
配置application.yml
1 | spring: |
测试
1 | // 实体类 |
1 | // mapper接口 |
1 | // 测试类 |
加入日志功能
1 | mybatis-plus: |
基本CRUD
BaseMapper
MyBatis-Plus中的基本CRUD在内置的BaseMapper中都已得到了实现,可以直接使用。
插入
1 |
|
最终执行的结果,所获取的id为1546026996496371714
这是因为MyBatis-Plus在实现插入数据时,会默认基于雪花算法的策略生成id
删除
通过id删除记录
1
2
3
4
5
6
public void testDeleteById(){
// DELETE FROM t_user WHERE id=?
int result = userMapper.deleteById(1546026996496371714L); // 注意要加L
System.out.println("受影响行数:"+result); // 1
}通过id批量删除记录
1
2
3
4
5
6
7
public void testDeleteBatchIds(){
List<Long> idList = Arrays.asList(2L, 3L);
// DELETE FROM t_user WHERE id IN ( ? , ? )
int result = userMapper.deleteBatchIds(idList);
System.out.println("受影响行数:"+result); // 2
}通过map条件删除记录
1
2
3
4
5
6
7
8
9
public void testDeleteByMap(){
Map<String, Object> map = new HashMap<>();
map.put("name", "张三");
map.put("age", 23);
// DELETE FROM t_user WHERE name = ? AND age = ?
int result = userMapper.deleteByMap(map);
System.out.println("受影响行数:"+result); // 1
}
修改
1 |
|
查询
根据id查询记录
1
2
3
4
5
6
public void testSelectById(){
// SELECT id,name,age,email FROM t_user WHERE id=?
User user = userMapper.selectById(4L);
System.out.println(user); // User(id=4, name=赵六, age=22, email=zhangsan@qq.com)
}根据多个id查询多条记录
1
2
3
4
5
6
7
8
9
public void testSelectBatchIds(){
List<Long> idList = Arrays.asList(4L, 5L);
// SELECT id,name,age,email FROM t_user WHERE id IN ( ? , ? )
List<User> list = userMapper.selectBatchIds(idList);
list.forEach(System.out::println);
// User(id=4, name=赵六, age=22, email=zhangsan@qq.com)
// User(id=5, name=张三, age=20, email=zhangsan@qq.com)
}通过map条件查询记录
1
2
3
4
5
6
7
8
9
public void testSelectByMap(){
Map<String, Object> map = new HashMap<>();
map.put("name", "赵六");
map.put("age", 22);
// SELECT id,name,age,email FROM t_user WHERE name = ? AND age = ?
List<User> list = userMapper.selectByMap(map);
list.forEach(System.out::println); // User(id=4, name=赵六, age=22, email=zhangsan@qq.com)
}查询所有记录
1
2
3
4
5
6
7
8
9
public void testSelectList(){
// SELECT id,name,age,email FROM t_user
List<User> list = userMapper.selectList(null);
list.forEach(System.out::println);
// User(id=4, name=赵六, age=22, email=zhangsan@qq.com)
// User(id=5, name=张三, age=20, email=zhangsan@qq.com)
}
通用Service
- 通用 Service CRUD 封装IService接口,进一步封装 CRUD 采用 get 查询单行 remove 删除 list 查询集合 page 分页 前缀命名方式区分 Mapper 层避免混淆
- 泛型 T 为任意实体对象
- 建议如果存在自定义通用 Service 方法的可能,请创建自己的 IBaseService 继承 Mybatis-Plus 提供的基类
- 官网地址
IService
MyBatis-Plus中有一个接口 IService和其实现类 ServiceImpl,封装了常见的业务层逻辑 详情查看源码IService和ServiceImpl
创建Service接口和实现类
1 | /** |
1 | /** |
测试
查询记录数
1
2
3
4
5
6
7
8
9
private UserService userService;
public void testGetCount(){
// SELECT COUNT( * ) FROM t_user
long count = userService.count();
System.out.println("总记录数:" + count);
}测试批量插入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void testSaveBatch(){
// SQL长度有限制,海量数据插入单条SQL无法实行,
// 因此MP将批量插入放在了通用Service中实现,而不是通用Mapper
ArrayList<User> users = new ArrayList<>();
for (int i = 0; i < 5; i++) {
User user = new User();
user.setName("abc" + i);
user.setAge(20 + i);
users.add(user);
}
// INSERT INTO t_user ( id, name, age ) VALUES ( ?, ?, ? )
userService.saveBatch(users);
}
常用注解
@TableName
在实体类类型上添加@TableName(“t_user”),标识实体类对应的表。
1 |
|
也可以通过全局配置文件解决
1 | mybatis-plus: |
@TableId
经过以上的测试,MyBatis-Plus在实现CRUD时,会默认将id作为主键列,并在插入数据时,默认基于雪花算法的策略生成id
在实体类中属性上通过@TableId标识为主键
1 |
|
value属性
若实体类中主键对应的属性为id,而表中表示主键的字段为uid,此时需要通过@TableId注解的value属性,指定表中的主键字段,@TableId(“uid”) 或 @TableId(value=”uid”)
type属性
type属性用来定义主键策略
值 | 描述 |
---|---|
IdType.ASSIGN_ID(默 认) | 基于雪花算法的策略生成数据id,与数据库id是否设置自增无关 |
IdType.AUTO | 使用数据库的自增策略,注意,该类型请确保数据库设置了id自增, 否则无效 |
配置全局主键策略:
1 | mybatis-plus: |
@TableField
MyBatis-Plus在执行SQL语句时,要保证实体类中的属性名和表中的字段名一致。
注意:
- 若实体类中的属性使用的是驼峰命名风格,而表中的字段使用的是下划线命名风格 例如实体类属性userName,表中字段user_name 此时MyBatis-Plus会自动将下划线命名风格转化为驼峰命名风格
- 若实体类中的属性和表中的字段不满足上述情况,例如实体类属性name,表中字段username 此时需要在实体类属性上使用@TableField(“username”)设置属性所对应的字段名
1 |
|
@TableLogic
逻辑删除
- 物理删除:真实删除,将对应数据从数据库中删除,之后查询不到此条被删除的数据
- 逻辑删除:假删除,将对应数据中代表是否被删除字段的状态修改为“被删除状态”,之后在数据库 中仍旧能看到此条数据记录
- 使用场景:可以进行数据恢复
实现逻辑删除
数据库中创建逻辑删除状态列,设置默认值为0
实体类中添加逻辑删除属性
1
2
3
4
5
6
7
8
9
10
public class User {
private Long id;
private String name;
private Integer age;
private String email;
// 逻辑删除
private Integer isDeleted;
}测试
测试删除功能,真正执行的是修改,将is_deleted改成1
测试查询功能,被逻辑删除的数据默认不会被查询,结果为null
条件构造器和常用接口
wapper介绍
- Wrapper : 条件构造抽象类,最顶端父类
- AbstractWrapper : 用于查询条件封装,生成 sql 的 where 条件
- QueryWrapper : 查询条件封装
- UpdateWrapper : Update 条件封装
- AbstractLambdaWrapper : 使用Lambda 语法
- LambdaQueryWrapper :用于Lambda语法使用的查询Wrapper
- LambdaUpdateWrapper : Lambda 更新封装Wrapper
- AbstractWrapper : 用于查询条件封装,生成 sql 的 where 条件
QueryWrapper
组装查询条件
1 |
|
组装排序条件
1 |
|
组装删除条件
1 |
|
条件的优先级
1 |
|
1 |
|
组装select子句
1 |
|
实现子查询
1 |
|
UpdateWrapper
1 |
|
condition
在真正开发的过程中,组装条件是常见的功能,而这些条件数据来源于用户输入,是可选的,因此我们在组装这些条件时,必须先判断用户是否选择了这些条件,若选择则需要组装该条件
思路一
1 |
|
思路二
1 |
|
LambdaQueryWrapper
1 |
|
LambdaUpdateWrapper
1 |
|
插件
分页插件
MyBatis Plus自带分页插件,只要简单的配置即可实现分页功能
添加配置类
1
2
3
4
5
6
7
8
9
10
11
// @MapperScan("com.cuc.mybatisplus.mapper") //可以将主类中的注解移到此处
public class MybatisPlusConfig {
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void testPage(){
//设置分页参数
Page<User> page = new Page<>(1, 5);
userMapper.selectPage(page, null);
//获取分页数据
List<User> list = page.getRecords();
list.forEach(System.out::println);
System.out.println("当前页:"+page.getCurrent());
System.out.println("每页显示的条数:"+page.getSize());
System.out.println("总记录数:"+page.getTotal());
System.out.println("总页数:"+page.getPages());
System.out.println("是否有上一页:"+page.hasPrevious());
System.out.println("是否有下一页:"+page.hasNext());
}
xml自定义分页
UserMapper中定义接口方法
1
2
3
4
5
6
7
8
9
10
public interface UserMapper extends BaseMapper<User> {
/**
* 根据年龄查询用户列表,分页显示
* @param page 分页对象,xml中可以从里面进行取值,传递参数 Page 即自动分页,必须放在第一位
* @param age 年龄
*/
Page<User> selectPageVo(; Page<User> page, Integer age)
}UserMapper.xml中编写SQL
1
2
3
4
5
6
7<!--SQL片段-->
<sql id="BaseColumns">id,name,age,email</sql>
<!-- Page<User> selectPageVo(@Param("page") Page<User> page, @Param("age") Integer age); -->
<select id="selectPageVo" resultType="com.cuc.mybatisplus.pojo.User">
SELECT <include refid="BaseColumns"></include> FROM t_user WHERE age > #{age}
</select>测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void testSelectPageVo(){
//设置分页参数
Page<User> page = new Page<>(1, 5);
userMapper.selectPageVo(page, 10);
//获取分页数据
List<User> list = page.getRecords();
list.forEach(System.out::println);
System.out.println("当前页:"+page.getCurrent());
System.out.println("每页显示的条数:"+page.getSize());
System.out.println("总记录数:"+page.getTotal());
System.out.println("总页数:"+page.getPages());
System.out.println("是否有上一页:"+page.hasPrevious());
System.out.println("是否有下一页:"+page.hasNext());
}
乐观锁
场景
一件商品,成本价是80元,售价是100元。老板先是通知小刘,说你去把商品价格增加50元。小刘正在玩游戏,耽搁了一个小时。正好一个小时后,老板觉得商品价格增加到150元,价格太高,可能会影响销量。又通知小王,你把商品价格降低30元。
此时,小刘和小王同时操作商品后台系统。小刘操作的时候,系统先取出商品价格100元;小王 也在操作,取出的商品价格也是100元。小刘将价格加了50元,并将100+50=150元存入了数据库;小王将商品减了30元,并将100-30=70元存入了数据库。是的,如果没有锁,小刘的操作就完全被小王的覆盖了。
现在商品价格是70元,比成本价低10元。几分钟后,这个商品很快出售了1千多件商品,老板亏1万多。
乐观锁与悲观锁
上面的故事,如果是乐观锁,小王保存价格前,会检查下价格是否被人修改过了。如果被修改过了,则重新取出的被修改后的价格,150元,这样他会将120元存入数据库。
如果是悲观锁,小刘取出数据后,小王只能等小刘操作完之后,才能对价格进行操作,也会保证最终的价格是120元。
模拟修改冲突
数据库表
实体类
1
2
3
4
5
6
7
8
public class Product {
private long id;
private String name;
private Integer price;
private Integer version;
}mapper接口
1
2
3
4
public interface ProductMapper extends BaseMapper<Product> {
}测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void testConcurrentUpdate() {
//1、小刘
Product p1 = productMapper.selectById(1L);
System.out.println("小刘取出的价格:" + p1.getPrice()); // 100
//2、小王
Product p2 = productMapper.selectById(1L);
System.out.println("小王取出的价格:" + p2.getPrice()); // 100
//3、小刘将价格加了50元,存入了数据库
p1.setPrice(p1.getPrice() + 50);
productMapper.updateById(p1);
System.out.println("小刘修改结果:" + productMapper.selectById(1L)); // 150
//4、小王将商品减了30元,存入了数据库
p2.setPrice(p2.getPrice() - 30);
productMapper.updateById(p2);
System.out.println("小王修改结果:" + productMapper.selectById(1L)); // 70
//最后的结果
Product p3 = productMapper.selectById(1L);
//价格覆盖,最后的结果:70
System.out.println("最后的结果:" + p3.getPrice());
}
乐观锁实现流程
取出记录时,获取当前version
1
SELECT id,`name`,price,`version` FROM product WHERE id=1
更新时,version + 1,如果where语句中的version版本不对,则更新失败
1
UPDATE product SET price=price+50, `version`=`version` + 1 WHERE id=1 AND `version`=1
Mybatis-Plus实现乐观锁
修改实体类
1
2
3
4
5
6
7
8
9
public class Product {
private long id;
private String name;
private Integer price;
private Integer version;
}添加乐观锁插件配置
1
2
3
4
5
6
7
8
9
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//添加分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
//添加乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}测试
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
public void testConcurrentVersionUpdate() {
//小刘取数据
Product p1 = productMapper.selectById(1L);
//小王取数据
Product p2 = productMapper.selectById(1L);
//小刘修改 + 50
p1.setPrice(p1.getPrice() + 50);
// UPDATE t_product SET name=?, price=?, version=1 WHERE id=? AND version=0
productMapper.updateById(p1);
//小王修改 - 30
p2.setPrice(p2.getPrice() - 30);
// UPDATE t_product SET name=?, price=?, version=1 WHERE id=? AND version=0
int result = productMapper.updateById(p2);
if(result == 0){
//失败重试,重新获取version并更新
p2 = productMapper.selectById(1L);
p2.setPrice(p2.getPrice() - 30);
// UPDATE t_product SET name=?, price=?, version=2 WHERE id=? AND version=1
productMapper.updateById(p2);
}
//老板看价格
Product p3 = productMapper.selectById(1L);
System.out.println("老板看价格:" + p3.getPrice());
}
通用枚举
表中的有些字段值是固定的,例如性别(男或女),此时我们可以使用MyBatis-Plus的通用枚举来实现
数据库表添加字段
创建通用枚举类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public enum SexEnum {
MALE(1, "男"),
FEMALE(2, "女");
private Integer sex;
private String sexName;
SexEnum(Integer sex, String sexName) {
this.sex = sex;
this.sexName = sexName;
}
}配置扫描通用枚举
1
2
3mybatis-plus:
# 配置扫描通用枚举
type-enums-package: com.cuc.mybatisplus.enums修改实体类
1
2// 添加sex属性
private SexEnum sex;测试
1
2
3
4
5
6
7
8
9
public void testSexEnum(){
User user = new User();
user.setName("Enum");
user.setAge(20);
//设置性别信息为枚举项,会将@EnumValue注解所标识的属性值存储到数据库
user.setSex(SexEnum.MALE);
userMapper.insert(user);
}
代码生成器
相比mybatis的逆向工程,能逆向生成更多的东西。比如控制层、业务层、持久层等
引入依赖
1
2
3
4
5
6
7
8
9
10<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.31</version>
</dependency>快速生成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24public class FastAutoGeneratorTest {
public static void main(String[] args) {
FastAutoGenerator
.create("jdbc:mysql://localhost:3306/db_mybatisplus?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8", "root", "root")
.globalConfig(builder -> {
builder.author("swj") // 设置作者
//.enableSwagger() // 开启 swagger 模式
.fileOverride() // 覆盖已生成文件
.outputDir("C:\\Users\\usesr\\Desktop\\test"); // 指定输出目录
})
.packageConfig(builder -> {
builder.parent("com.cuc") // 设置父包名
.moduleName("mybatisplus") // 设置父包模块名
.pathInfo(Collections.singletonMap(OutputFile.mapperXml, "C:\\Users\\usesr\\Desktop\\test")); // 设置mapperXml生成路径
})
.strategyConfig(builder -> {
builder.addInclude("t_user") // 设置需要生成的表名
.addTablePrefix("t_", "c_"); // 设置过滤表前缀
})
.templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker 引擎模板,默认的是Velocity引擎模板
.execute();
}
}
多数据源
适用于多种场景:纯粹多库、 读写分离、 一主多从、 混合模式等 目前我们就来模拟一个纯粹多库的一个场景,其他场景类似
场景说明:
我们创建两个库,分别为db_mybatisplus(以前的库不动)与db_mybatisplus_1(新建),通过一个测试用例分别获取db_mybatisplus里的用户数据与db_mybatisplus_1里的商品数据,如果获取到说明多库模拟成功
数据库表
引入依赖
1
2
3
4
5<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.5.0</version>
</dependency>配置多数据源
说明:注释掉之前的数据库连接,添加新配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22spring:
datasource:
# url: jdbc:mysql://localhost:3306/db_mybatisplus?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
# username: root
# password: root
# driver-class-name: com.mysql.cj.jdbc.Driver
dynamic:
# 设置默认的数据源或者数据源组,默认值即为master
primary: master
# 严格匹配数据源,默认false.true未匹配到指定数据源时抛异常,false使用默认数据源
strict: false
datasource:
master:
url: jdbc:mysql://localhost:3306/db_mybatisplus?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
slave_1:
url: jdbc:mysql://localhost:3306/db_mybatisplus_1?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver创建用户service
1
2public interface UserService extends IService<User> {
}1
2
3
4
//指定操作的数据源
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}创建商品service
1
2public interface ProductService extends IService<Product> {
}1
2
3
4
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService {
}测试
1
2
3
4
5
public void testDynamicDataSource(){
System.out.println(userService.getById(4L));
System.out.println(productService.getById(1L));
}
MyBatisX插件
MyBatis-Plus为我们提供了强大的mapper和service模板,能够大大的提高开发效率
但是在真正开发过程中,MyBatis-Plus并不能为我们解决所有问题,例如一些复杂的SQL,多表联查,我们就需要自己去编写代码和SQL语句,我们该如何快速的解决这个问题呢,这个时候可以使用MyBatisX插件
MyBatisX一款基于 IDEA 的快速开发插件,为效率而生。
MyBatisX插件用法:https://baomidou.com/pages/ba5b24/