MybatisPlus入门

551 阅读7分钟

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支持的类型:

常用类型:

  1. AUTO:自增
  2. ASSIGN_ID:雪花算法生成long类型id【19位】

雪花算法

雪花算法是 64 位 的二进制,一共包含了四部分:

  • 1位是符号位,也就是最高位,始终是0,没有任何意义,因为要是唯一计算机二进制补码中就是负数,0才是正数。
  • 41位是时间戳,具体到毫秒,41位的二进制可以使用69年,因为时间理论上永恒递增,所以根据这个排序是可以的。
  • 10位是机器标识,可以全部用作机器ID,也可以用来标识机房ID + 机器ID,10位最多可以表示1024台机器。
  • 12位是计数序列号,也就是同一台机器上同一时间,理论上还可以同时生成不同的ID,12位的序列号能够区分出4096个ID。

时间回拨问题

在获取时间的时候,可能会出现时间回拨的问题,什么是时间回拨问题呢?就是服务器上的时间突然倒退到之前的时间。

  1. 人为原因,把系统环境的时间改了。
  2. 有时候不同的机器上需要同步时间,可能不同机器之间存在误差,那么可能会出现时间回拨问题。

解决方案

  1. 回拨时间小的时候,不生成 ID,循环等待到时间点到达。
  2. 上面的方案只适合时钟回拨较小的,如果间隔过大,阻塞等待,肯定是不可取的,因此要么超过一定大小的回拨直接报错,拒绝服务,或者有一种方案是利用拓展位,回拨之后在拓展位上加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());
    }