1. 入门
MyBatis-Plus 🚀 为简化开发而生 (baomidou.com)
1.1. 引入依赖
<!-- MyBatis Plus 最新版本 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<!-- MyBatis 核心库版本 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.11</version>
</dependency>
1.2. 常见注解
1.2.1. @TableName
使用位置:实体类
描述:标识实体类对应的表
1.2.2. @TableId
使用位置:实体类的主键字段
描述:标识实体类中的主键字段
支持的属性:value,type
IdType支持的类型:
常用类型:
- AUTO:自增
- ASSIGN_ID:雪花算法生成long类型id【19位】
雪花算法
雪花算法是 64 位 的二进制,一共包含了四部分:
- 1位是符号位,也就是最高位,始终是0,没有任何意义,因为要是唯一计算机二进制补码中就是负数,0才是正数。
- 41位是时间戳,具体到毫秒,41位的二进制可以使用69年,因为时间理论上永恒递增,所以根据这个排序是可以的。
- 10位是机器标识,可以全部用作机器ID,也可以用来标识机房ID + 机器ID,10位最多可以表示1024台机器。
- 12位是计数序列号,也就是同一台机器上同一时间,理论上还可以同时生成不同的ID,12位的序列号能够区分出4096个ID。
时间回拨问题
在获取时间的时候,可能会出现时间回拨的问题,什么是时间回拨问题呢?就是服务器上的时间突然倒退到之前的时间。
- 人为原因,把系统环境的时间改了。
- 有时候不同的机器上需要同步时间,可能不同机器之间存在误差,那么可能会出现时间回拨问题。
解决方案
- 回拨时间小的时候,不生成 ID,循环等待到时间点到达。
- 上面的方案只适合时钟回拨较小的,如果间隔过大,阻塞等待,肯定是不可取的,因此要么超过一定大小的回拨直接报错,拒绝服务,或者有一种方案是利用拓展位,回拨之后在拓展位上加1就可以了,这样ID依然可以保持唯一。但是这个要求我们提前预留出位数,要么从机器id中,要么从序列号中,腾出一定的位,在时间回拨的时候,这个位置
+1。
1.2.3. @TableField
使用位置:实体类字段上
常用属性:
value:数据库字段名
exist:是否为数据库表字段。表中没有的字段
select:是否进行select查询。如不想查密码
1.3. 常见配置
mybatis-plus:
mapper-locations: classpath*:mapper/*.xml #mapper.xml文件地址
global-config:
db-config:
id-type: auto #全局配置针对所有mp配置,局部只针对单个。局部优先于全局
table-prefix: tb_ #表前缀
type-aliases-package: com.itheima.mp.domain.po #实体类的别名扫描包
2. 核心功能
2.1. 条件构造器
支持链式编程
2.1.1. QueryWrapper
无论是修改、删除、查询,都可以使用QueryWrapper来构建查询条件。
查询名字中带o,存款大于等于1000,以"139"开头的手机号的人
void testQueryWrapper1() {
//查询名字中带o,存款大于等于1000
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.like("username", "o").ge("balance", 1000)
.likeLeft("phone", "139");
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}
更新:更新用户名为jack的用户的余额为1000
void testQueryWrapper2() {
//
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.like("username", "jack");
User user = new User();
user.setBalance(1000);
userMapper.update(user, wrapper);
}
2.1.2. UpdateWrapper
基于BaseMapper中的update方法更新时只能直接赋值,对于一些复杂的需求就难以实现。
例如:更新id为1,2,5的用户的余额,扣200,对应的SQL应该是:
UPDATE user SETbalance= balance - 200 WHERE id in(1, 2, 5)
void testUpdateWrapper() {
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
updateWrapper.setSql("balance=balance+500").in("id", 1, 2, 5);
// 2.更新,注意第一个参数可以给null,也就是不填更新字段和数据,
// 而是基于UpdateWrapper中的setSQL来更新
userMapper.update(null, updateWrapper);
}
2.1.3. LambdaQueryWrapper
无论是QueryWrapper还是UpdateWrapper在构造条件的时候都需要写死字段名称,会出现字符串魔法值。这在编程规范中显然是不推荐的。
其中一种办法是基于变量的gettter方法结合反射技术。因此我们只要将条件对应的字段的getter方法传递给MybatisPlus,它就能计算出对应的变量名了。而传递方法可以使用JDK8中的方法引用和Lambda表达式。
因此MybatisPlus又提供了一套基于Lambda的Wrapper,包含两个:
分别对应QueryWrapper和UpdateWrapper
使用如下:
void testLambdaQueryWrapper1() {
//查询名字中带o,存款大于等于1000
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.lambda().select(User::getUsername, User::getBalance,User::getPhone)
.like(User::getUsername, "o")
.ge(User::getBalance, 1000)
.likeLeft(User::getPhone, "139");
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}
2.2. 自定义SQL
2.2.1. 基本用法
标记参数@Param("ew")
sql语句中加${ew.customSqlSegment}
在mapper中自定义SQL
public interface UserMapper extends BaseMapper<User> {
@Select("UPDATE user SET balance = balance - #{money} ${ew.customSqlSegment}")
voiddeductBalanceByIds(@Param("money")int money, @Param("ew") QueryWrapper<User> wrapper);
void testCustomWrapper() {
List<Long> ids = List.of(1L, 2L, 4L);
QueryWrapper<User> wrapper = newQueryWrapper<User>().in("id", ids);
在mapper中自定义SQL
@Select("SELECT u.* FROM user u INNER JOIN address a ON u.id = a.user_id ${ew.customSqlSegment}")
List<User> queryUserByWrapper(@Param("ew")QueryWrapper<User> wrapper);
void testCustomJoinWrapper() {
// 1.准备自定义查询条件
QueryWrapper<User> wrapper = newQueryWrapper<User>()
.in("u.id", List.of(1L, 2L, 4L))
.eq("a.city", "北京");
// 2.调用mapper的自定义方法
List<User> users = userMapper.queryUserByWrapper(wrapper);
users.forEach(System.out::println);
}
2.3. Service接口
2.3.1. 基本用法
自定义Service接口,继承IService。自定义Service实现类继承ServiceImpl<mapper,entity>,实现自定义Service。
自定义Mapper,继承BaseMapper。填充泛型为实体类
2.3.2. 批量插入
@Test
void testSaveOneByOne() {
longb= System.currentTimeMillis();
for (inti=1; i <= 100000; i++) {
userService.save(buildUser(i));
}
longe= System.currentTimeMillis();
System.out.println("耗时:" + (e - b));
}
private User buildUser(int i) {
User user=new User();
user.setUsername("user_" + i);
user.setPassword("123");
user.setPhone("" + (18688190000L + i));
user.setBalance(2000);
user.setInfo("{"age": 24, "intro": "英文老师", "gender": "female"}");
user.setCreateTime(LocalDateTime.now());
user.setUpdateTime(user.getCreateTime());
return user;
}
@Test
void testSaveBatch() {
// 准备10万条数据
List<User> list = new ArrayList<>(1000);
longb= System.currentTimeMillis();
for (inti=1; i <= 100000; i++) {
list.add(buildUser(i));
// 每1000条批量插入一次
if (i % 1000 == 0) {
userService.saveBatch(list);
list.clear();
}
}
long e= System.currentTimeMillis();
System.out.println("耗时:" + (e - b));
}
private User buildUser(int i) {
User user=new User();
user.setUsername("user_" + i);
user.setPassword("123");
user.setPhone("" + (18688190000L + i));
user.setBalance(2000);
user.setInfo("{"age": 24, "intro": "英文老师", "gender": "female"}");
user.setCreateTime(LocalDateTime.now());
user.setUpdateTime(user.getCreateTime());
return user;
}
MybatisPlus的批处理是基于PrepareStatement的预编译模式,然后批量提交,最终在数据库执行时还是会有多条insert语句,逐条插入数据。
而如果想要得到最佳性能,最好是将多条SQL合并为一条,像这样:
MySQL的客户端连接参数中有这样的一个参数:rewriteBatchedStatements。顾名思义,就是重写批处理的statement语句。
修改项目中的application.yml文件,在jdbc的url后面添加参数&rewriteBatchedStatements=true:
IService.saveBatch
- jdbcUrl后面加一个参数:rewriteBatchedStatements=true
2.3.3. Lambda
LambdaQuery,LambdaUpdate
条件:like,eq,between,set
链式结尾:list,one,count,update
3. 扩展功能
3.1. 逻辑删除
对于一些比较重要的数据,我们往往会采用逻辑删除的方案,
- 在表中添加一个字段标记数据是否被删除
- 当删除数据时把标记置为true
- 查询时过滤掉标记为true的数据
表添加deleted逻辑删除字段,默认值0。实体类添加deleted字段
mybatis-plus:
global-config:
db-config:
logic-delete-value: 1
logic-delete-field: deleted
logic-not-delete-value: 0
底层SQL逻辑改变
查询SQL逻辑也改变,对deleted做了判断
3.2. 通用枚举
MybatisPlus提供了一个处理枚举的类型转换器,可以帮我们把枚举类型与数据库类型自动转换。
package com.itheima.mp.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.Getter;
@Getter
public enum UserStatus {
NORMAL(1, "正常"),
FREEZE(2, "冻结");
@EnumValue
private final int value;
@JsonValue
private final String desc;
UserStatus(int value, String desc) {
this.value = value;
this.desc = desc;
}
}
把User类中的status字段改为UserStatus 类型
要让MybatisPlus处理枚举与数据库类型自动转换,MybatisPlus提供了@EnumValue注解来标记枚举属性
查询出的status字段是枚举类型
在UserStatus枚举中通过@JsonValue注解标记JSON序列化时展示的字段
4. 分页插件
package com.itheima.mp.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
// 初始化核心插件
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
public void test() {
Page<User> page = new Page<>(1, 10);
page.addOrder(new OrderItem("id", false)).addOrder(OrderItem.asc("username"));
userService.lambdaQuery().page(page);
System.out.println("总条数:" + page.getTotal());
System.out.println("总页数:" + page.getPages());
System.out.println("页大小" + page.getSize());
System.out.println("当前页码" + page.getCurrent());
System.out.println("数据" + page.getRecords());
}