MybatisPlus知识查漏补缺

简介

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

特性

  • 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
  • 损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作
  • 强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求
  • 支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错
  • 支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题
  • 支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作
  • 支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )
  • 内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用
  • 内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询
  • 分页插件支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库
  • 内置性能分析插件:可输出 SQL 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询
  • 内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作

框架结构

环境搭建

记录Spring+MybatisPlus环境的搭建(完全注解)

添加Spring配置核心类

1
2
3
4
@Configuration
@ComponentScan("com.zhuweitung")
public class SpringConfig {
}

添加Mybatis配置核心类

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
@Configuration
@MapperScan(basePackages = "com.zhuweitung.mp.mapper")
@PropertySource("classpath:jdbc.properties")
public class MybatisConfig {

@Value("${dataSourceClassName}")
private String dataSourceClassName;
@Value("${jdbc.jdbcUrl}")
private String jdbcUrl;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
@Value("${minimumIdle}")
private Integer minimumIdle;
@Value("${maximumPoolSize}")
private Integer maximumPoolSize;

@Bean
public DataSource dataSource() {
Properties properties = new Properties();
properties.put("dataSourceClassName", dataSourceClassName);
properties.put("dataSource.jdbcUrl", jdbcUrl);
properties.put("dataSource.username", username);
properties.put("dataSource.password", password);
properties.put("minimumIdle", minimumIdle);
properties.put("maximumPoolSize", maximumPoolSize);
return new HikariDataSource(new HikariConfig(properties));
}

@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean sessionFactoryBean = new MybatisSqlSessionFactoryBean();
sessionFactoryBean.setDataSource(dataSource);
return sessionFactoryBean.getObject();
}

}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
@SpringJUnitConfig({SpringConfig.class, MybatisConfig.class})
public class MyBatisPlusTest {

@Autowired
private UserMapper userMapper;

@Test
public void selectList() {
List<User> users = userMapper.selectList(null);
System.out.println(users.size());
}

}

常用注解

  • @TableName:指定类所表示的表名

  • TableId:标明主键

    • value:若属性名与数据库字段名称不一致,可以使用此属性

    • type:主键生成策略

      主键生成策略 描述
      IdType.ASSIGN_ID(默认) 基于雪花算法的策略生成数据id,与数据库id是否设置自增无关
      IdType.AUTO 使用数据库的自增策略,注意,该类型请确保数据库设置了id自增
  • @TbaleField:指定属性所表示的字段名

  • @TableLogic:指定逻辑删除标识符

条件构造器

  • Wrapper : 条件构造抽象类,最顶端父类
    • AbstractWrapper : 用于查询条件封装,生成 sql 的 where 条件
    • QueryWrapper : 查询条件封装
    • UpdateWrapper : Update 条件封装
    • AbstractLambdaWrapper : 使用Lambda 语法
      • LambdaQueryWrapper :用于Lambda语法使用的查询Wrapper
      • LambdaUpdateWrapper : Lambda 更新封装Wrapper

QueryWrapper

组装查询排序条件
1
2
3
4
5
6
7
QueryWrapper<TUser> query = new QueryWrapper<>();
query.like("name", "ma")
.ge("age", 22)
.le("age", 30)
.orderByAsc("age")
.orderByDesc("name");
List<TUser> users = tUserService.list(query);
条件的优先级
1
2
3
4
5
// 查询用户名包含zl且(年龄大于20或邮箱为null)的用户信息
QueryWrapper<TUser> query = new QueryWrapper<>();
query.like("name", "zl")
.and(i -> i.ge("age", 20).or().isNull("email"));
List<TUser> users = tUserService.list(query);
组装select子句
1
2
3
4
5
QueryWrapper<TUser> query = new QueryWrapper<>();
query.like("name", "zl")
.and(i -> i.ge("age", 20).or().isNull("email"))
.select("name", "age", "email");
List<Map<String, Object>> maps = tUserService.listMaps(query);
实现子查询
1
2
3
QueryWrapper<TUser> query = new QueryWrapper<>();
query.inSql("id", "select id from t_user where name like '%op%'");
List<TUser> users = tUserService.list(query);

UpdateWrapper

1
2
3
4
5
UpdateWrapper<TUser> update = new UpdateWrapper<>();
update.like("name", "zl")
.and(i -> i.ge("age", 20).or().isNull("email"))
.set("email", "no-reply@kedr.cc");
tUserService.update(null, update);

UpdateWrapper不仅拥有QueryWrapper的组装条件功能,还提供了set方法进行修改对应条件的数据库信息

condition

1
2
3
4
5
6
7
QueryWrapper<TUser> query = new QueryWrapper<>();
String nameLike = null;
Integer ageLe = 21;
query.like(StrUtil.isNotEmpty(nameLike), "name", nameLike)
.le(ageLe != null, "age", ageLe);
List<TUser> users = tUserService.list(query);
users.forEach(u -> log.info("{}", u));

先判断用户是否选择了这些条件,若选择则需要组装该条件,若没有选择则一定不能组装,以免影响SQL执行的结果

LambdaQueryWrapper

1
2
3
4
5
6
LambdaQueryWrapper<TUser> query = new LambdaQueryWrapper<>();
query.like(TUser::getName, "zl")
.and(i -> i.ge(TUser::getAge, 20).or().isNull(TUser::getEmail))
.select(TUser::getName, TUser::getAge, TUser::getEmail);
List<Map<String, Object>> maps = tUserService.listMaps(query);
maps.forEach(m -> log.info("{}", m));

功能等同于QueryWrapper,提供了Lambda表达式的语法可以避免填错列名

LambdaUpdateWrapper

1
2
3
4
5
LambdaUpdateWrapper<TUser> update = new LambdaUpdateWrapper<>();
update.like(TUser::getName, "zl")
.and(i -> i.ge(TUser::getAge, 20).or().isNull(TUser::getEmail))
.set(TUser::getEmail, "no-reply@kedr.cc");
tUserService.update(null, update);

功能等同于UpdateWrapper,提供了Lambda表达式的语法可以避免填错列名

常用插件

分页插件

mybatis-plus自带分页插件

使用方式

在配置类中添加

1
2
3
4
5
6
7
8
9
10
11
12
13
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean sessionFactoryBean = new MybatisSqlSessionFactoryBean();
sessionFactoryBean.setDataSource(dataSource);

MybatisConfiguration configuration = new MybatisConfiguration();
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
configuration.addInterceptor(interceptor);
sessionFactoryBean.setConfiguration(configuration);
return sessionFactoryBean.getObject();
}

使用

1
2
3
4
5
6
7
8
@Test
public void selectPage() {
LambdaQueryWrapper<TUser> query = new LambdaQueryWrapper<>();
query.like(TUser::getName, "zl")
.and(i -> i.ge(TUser::getAge, 20).or().isNull(TUser::getEmail));
Page<TUser> page = tUserService.page(new Page<>(1, 10), query);
page.getRecords().forEach(u -> log.info("{}", u));
}

自定义方法启用分页

mapper和mapper.xml中分别添加查询方法

1
2
3
4
5
6
7
8
9
10
<!-- Page<TUser> selectPageVo(@Param("page") Page<TUser> page, @Param("age") Integer age); -->
<select id="selectPageVo" resultType="com.zhuweitung.mp.model.TUser">
select *
from t_user
<where>
<if test="age != null">
and age >= #{age}
</if>
</where>
</select>

使用

1
2
Page<TUser> page = tUserMapper.selectPageVo(new Page<>(2, 10), 21);
page.getRecords().forEach(u -> log.info("{}", u));

乐观锁和悲观锁

乐观锁

乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法);

乐观锁通常多于写比较少的情况下(多读场景),避免频繁加锁影响性能,大大提升了系统的吞吐量;

Java中的java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的;

mybatis-plus的乐观锁实现方式:

  • 取出记录时,获取当前 version
  • 更新时,带上这个 version
  • 执行更新时, set version = newVersion where version = oldVersion
  • 如果 version 不对,就更新失败
悲观锁

悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放;

即共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程;

Java 中synchronizedReentrantLock等独占锁就是悲观锁思想的实现;

开启乐观锁插件

实体类添加@Version注解

1
2
3
4
5
6
7
8
@Data
public class Product {
private Long id;
private String name;
private Integer price;
@Version
private Integer version;
}

添加乐观锁插件配置

1
2
3
4
// 添加乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
configuration.addInterceptor(interceptor);
sessionFactoryBean.setConfiguration(configuration);

通用枚举

  • 创建枚举类

    • 方式一:使用@EnumValue标记需要存储到数据库的属性

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      @Getter
      public enum UserSex {
      FEMALE(0, "女"),
      MALE(1, "男");

      // 将注解所标识的属性的值存储到数据库中
      @EnumValue
      private final Integer value;
      private final String name;

      UserSex(Integer value, String name) {
      this.value = value;
      this.name = name;
      }
      }
    • 方式二:实现IEnum<T>接口,getValue()方法标记存储到数据库的值

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      @Getter
      public enum UserSex2 implements IEnum<Integer> {
      FEMALE(0, "女"),
      MALE(1, "男");

      private final Integer value;
      private final String name;

      UserSex2(Integer value, String name) {
      this.value = value;
      this.name = name;
      }

      @Override
      public Integer getValue() {
      return this.value;
      }
      }
  • 实体类属性使用枚举类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    @Getter
    @Setter
    @TableName("t_user")
    public class TUser {

    @TableId
    private Integer id;

    private String name;

    private UserSex sex;

    private String password;

    private Integer age;

    private String email;

    @TableLogic
    private Integer isDeleted;
    }
  • 配置全局默认枚举类型处理器

    1
    2
    3
    4
    5
    6
    // ...
    MybatisConfiguration configuration = new MybatisConfiguration();
    // 配置全局默认枚举类型处理器
    configuration.setDefaultEnumTypeHandler(MybatisEnumTypeHandler.class);
    sessionFactoryBean.setConfiguration(configuration);
    // ...

多数据源

适用场景:纯粹多库、 读写分离、 一主多从、 混合模式等

下面以spring-boot项目作为配置示例

引入依赖

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
spring:
# 配置数据源信息
datasource:
dynamic:
# 设置默认的数据源或者数据源组,默认值即为master
primary: master
# 严格匹配数据源,默认false.true未匹配到指定数据源时抛异常,false使用默认数据源
strict: false
datasource:
master:
url: jdbc:mysql://localhost:3306/tmp1?characterEncoding=utf-8&useSSL=false
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
slave_1:
url: jdbc:mysql://localhost:3306/tmp2?characterEncoding=utf-8&useSSL=false
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456

在Service上指定操作的数据源

1
2
3
4
@DS("master") // 指定操作的数据源,user表master库
public interface UserService extends IService<User> {}
@DS("slave_1") // 指定操作的数据源,product表slave_1库
public interface ProductService extends IService<Product> {}

字段类型处理器

类型处理器,用于 JavaType 与 JdbcType 之间的转换,用于 PreparedStatement 设置参数值和从 ResultSet 或 CallableStatement 中取出一个值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Data
@Accessors(chain = true)
@TableName(autoResultMap = true)
public class User {
private Long id;

/**
* 注意!! 必须开启映射注解
*
* @TableName(autoResultMap = true)
*
* 以下两种类型处理器,二选一 也可以同时存在
*
* 注意!!选择对应的 JSON 处理器也必须存在对应 JSON 解析依赖包
*/
@TableField(typeHandler = JacksonTypeHandler.class)
// @TableField(typeHandler = FastjsonTypeHandler.class)
private OtherInfo otherInfo;

}

自动填充功能

原理:

  • 实现元对象处理器接口:com.baomidou.mybatisplus.core.handlers.MetaObjectHandler

  • 注解填充字段 @TableField(fill = FieldFill.INSERT) 生成器策略部分也可以配置

    1
    2
    3
    4
    5
    6
    public class User {

    // 注意!这里需要标记为填充字段
    @TableField(fill = FieldFill.INSERT)
    private String fillField;
    }
  • 自定义实现类 MyMetaObjectHandler

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Component
    public class MyMetaObjectHandler implements MetaObjectHandler {

    @Override
    public void insertFill(MetaObject metaObject) {
    this.strictInsertFill(metaObject, "createTime", () -> LocalDateTime.now(), LocalDateTime.class);
    }

    @Override
    public void updateFill(MetaObject metaObject) {
    this.strictUpdateFill(metaObject, "updateTime", () -> LocalDateTime.now(), LocalDateTime.class);
    }
    }

拓展知识

雪花算法

背景

需要选择合适的方案去应对数据规模的增长,以应对逐渐增长的访问压力和数据量。 数据库的扩展方式主要包括:业务分库、主从复制,数据库分表。

数据库分表

将不同业务数据分散存储到不同的数据库服务器,能够支撑百万甚至千万用户规模的业务,但如果业务 继续发展,同一业务的单表数据也会达到单台数据库服务器的处理瓶颈。例如,淘宝的几亿用户数据, 如果全部存放在一台数据库服务器的一张表中,肯定是无法满足性能要求的,此时就需要对单表数据进 行拆分。 单表数据拆分有两种方式:垂直分表和水平分表

  • 垂直分表:垂直分表适合将表中某些不常用且占了大量空间的列拆分出去

  • 水平分表:水平分表适合表行数特别大的表;水平分表相比垂直分表,会引入更多的复杂性,例如要求全局唯一的数据id该如何处理

    • 主键自增:以最常见的用户 ID 为例,可以按照 1000000 的范围大小进行分段,1 ~ 999999 放到表 1中, 1000000 ~ 1999999 放到表2中,以此类推

      • 复杂点:分段大小的选取,需要根据业务选取合适 的分段大小
      • 优点:可以随着数据的增加平滑地扩充新的表;如,现在的用户是 100 万,如果增加到 1000 万, 只需要增加新的表就可以了,原有的数据不需要动
      • 缺点:分布不均匀
    • 取模:同样以用户 ID 为例,假如我们一开始就规划了 10 个数据库表,可以简单地用 user_id % 10 的值来 表示数据所属的数据库表编号,ID 为 985 的用户放到编号为 5 的子表中,ID 为 10086 的用户放到编号 为 6 的子表中

      • 复杂点:初始表数量的确定;表数量太多维护比较麻烦,表数量太少又可能导致单表性能存在问题
      • 优点:表分布比较均匀
      • 缺点:扩充新的表很麻烦,所有数据都要重分布
    • 雪花算法:它能够保证不同表的主键的不重复性,以及相同表的 主键的有序性

      • 长度共64bit(一个long型)
        • 符号位,1bit
        • 时间戳位,41bit,毫秒级,存储的是时间截的差值(当前时间截 - 开始时间截),结果约等于69.73年
        • 机器ID(5bit是数据中心,5bit是机器ID),10bit,可以部署在1024个节点
        • 流水号,12bit号(意味着每个节点在每毫秒可以产生 4096 个 ID)
      • 优点:整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞,并且效率较高

MybatisPlus知识查漏补缺
https://blog.kedr.cc/posts/2685019757/
作者
zhuweitung
发布于
2021年6月9日
许可协议