飞天班第17节:企业项目研发(一)MyBatisPlus使用

2020/04/04

1、Mybatis-plus 简介

官网地址:https://mp.baomidou.com/guide/

MyBatis-Plus (简称 MP)是一个 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 操作智能分析阻断,也可自定义拦截规则,预防误操作

支持数据库

  • mysql 、 mariadb 、 oracle 、 db2 、 h2 、 hsql 、 sqlite 、 postgresql 、 sqlserver
  • 达梦数据库 、 虚谷数据库 、 人大金仓数据库

导入依赖中mysql5和mysq8的jdbc驱动区别需要注意

mysql5

spring.datasource.driver-class-name=com.mysql.sql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/mpdata?useSSL=false
spring.datasource.username=root
spring.datasource.password=123456

mysql8(Spring Boot2.1开始默认)

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/mpdata?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=123456

2、主键策略

MP有自己的默认对应规则,例如实体类名为:OwnUser,会给你对应到数据库中的own_user表。这是MP源代码中实现的实体与表的对应关系。如果你不想用默认的对应规则,可以使用@TableName()注解,进行表名指定。

关于id的一些生成策略,参考:https://www.cnblogs.com/haoxinyue/p/5208136.html

1、ID_WORKER

Mybatis-plus默认的主键策略是ID_WORKER (雪花算法)

snowflake(雪花算法)是Twitter开源的分布式ID生成算法,结果是一个long型的ID。其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0。具体实现的代码可以参看https://github.com/twitter/snowflake。

// 实体类 ID字段不加注解就是默认的ID_WORKER
@Data
public class User {
    
    private Long id;
    private String name;
    private Integer age;
    private String email;

    @TableField(fill = FieldFill.INSERT)
    private Date createTime;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Date updateTime;

    // 引入乐观锁插件
    @Version
    private Integer version;
    @TableLogic
    private Integer delFlag;
}

通过设置@TableId可以让Mybatis-plus自动为我们生成雪花算法的ID号,该ID号是一个长整型数据,非常方便。但是雪花算法的ID号是在Insert执行的时候生成的,我们在Insert执行前是不知道Entity会获得一个什么ID号。如果想提前获取一个雪花id,可以调用IdWorker 类

package com.baomidou.mybatisplus.core.toolkit;

import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator;
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;

/**
 * id 获取器
 *
 * @author hubin
 * @since 2016-08-01
 */
public class IdWorker {

    /**
     * 主机和进程的机器码
     */
    private static IdentifierGenerator IDENTIFIER_GENERATOR = new DefaultIdentifierGenerator();

    /**
     * 毫秒格式化时间
     */
    public static final DateTimeFormatter MILLISECOND = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS");

    /**
     * 获取唯一ID
     *
     * @return id
     */
    public static long getId() {
        return getId(new Object());
    }

    /**
     * 获取唯一ID
     *
     * @return id
     */
    public static long getId(Object entity) {
        return IDENTIFIER_GENERATOR.nextId(entity).longValue();
    }

    /**
     * 获取唯一ID
     *
     * @return id
     */
    public static String getIdStr() {
        return getIdStr(new Object());
    }

    /**
     * 获取唯一ID
     *
     * @return id
     */
    public static String getIdStr(Object entity) {
        return IDENTIFIER_GENERATOR.nextId(entity).toString();
    }

    /**
     * 格式化的毫秒时间
     */
    public static String getMillisecond() {
        return LocalDateTime.now().format(MILLISECOND);
    }

    /**
     * 时间 ID = Time + ID
     * <p>例如:可用于商品订单 ID</p>
     */
    public static String getTimeId() {
        return getMillisecond() + getIdStr();
    }

    /**
     * 有参构造器
     *
     * @param workerId     工作机器 ID
     * @param dataCenterId 序列号
     * @see #setIdentifierGenerator(IdentifierGenerator)
     */
    public static void initSequence(long workerId, long dataCenterId) {
        IDENTIFIER_GENERATOR = new DefaultIdentifierGenerator(workerId, dataCenterId);
    }

    /**
     * 自定义id 生成方式
     * @param identifierGenerator id 生成器
     * @see GlobalConfig#setIdentifierGenerator(IdentifierGenerator)
     */
    public static void setIdentifierGenerator(IdentifierGenerator identifierGenerator) {
        IDENTIFIER_GENERATOR = identifierGenerator;
    }

    /**
     * 使用ThreadLocalRandom获取UUID获取更优的效果 去掉"-"
     */
    public static String get32UUID() {
        ThreadLocalRandom random = ThreadLocalRandom.current();
        return new UUID(random.nextLong(), random.nextLong()).toString().replace(StringPool.DASH, StringPool.EMPTY);
    }
}

2、自增策略

  • 需要在创建数据表的时候设置主键自增

  • 实体类ID字段配置@TableId(type = IdType.AUTO)

    @Data
    public class User {
        //@TableId(type = IdType.AUTO)
        private Long id;
        private String name;
        private Integer age;
    

要想影响所有实体类的配置,可以在application.properties中设置全局主键配置

mybatis-plus.global-config.db-config.id-type=auto

3、其他主键策略,点击IdType源码得知

public enum IdType {
    AUTO(0),	// 数据库id自增
    NONE(1),	// 未设置主键类型
    INPUT(2),	// 用户手动setID,可以通过自己注册自动填充插件进行填充
  // 以下3种只有ID为空,才自动填充
    ID_WORKER(3),	
    UUID(4),
    ID_WORKER_STR(5);	// id_worker的字符串表示

    private final int key;

    private IdType(int key) {
        this.key = key;
    }

    public int getKey() {
        return this.key;
    }
}

3、自动填充

阿里的规范中说了,数据库表表必须有三个字段:ID、创建时间、更新时间,自动填充时间有两种方式实现。

  1. 实体类上添加注解

    @TableField(fill = FieldFill.INSERT)
    private Date createTime;
       
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Date updateTime;
    
  2. 新建一个handler包,实现元对象处理接口

    /**
     * 处理元数据
     * 1.实现 MetaObjectHandler
     * 2.添加到IOC容器中
     */
    @Slf4j
    @Component
    public class MybatisObjectHandler  implements MetaObjectHandler {
       
    	/**
    	 * 插入时候自动填充
    	 * @param metaObject
    	 */
    	@Override
    	public void insertFill(MetaObject metaObject) {
    		log.info("start insert fill");
    		//三个参数:
    		//实体类属性名
    		//要填充的内容,必须和实体类中字段类型相同
    		//metaObject
    		//System.out.println("MybatisObjectHandler =>" +metaObject);
    		this.setFieldValByName("createTime",new Date(),metaObject);
    		this.setFieldValByName("updateTime",new Date(),metaObject);
       
    	}
       
    	// 更新时自动填充
    	@Override
    	public void updateFill(MetaObject metaObject) {
    		log.info("start update fill");
    		this.setFieldValByName("updateTime",new Date(),metaObject);
    	}
    }
    
  3. 测试

4、乐观锁

面试中经常会问乐观锁

悲观锁:无论干啥,都先给你锁上,再做操作,比如传统的synchronized,juc的ReentrantLock就是悲观锁。

乐观锁:非常乐观,无论什么操作都不加锁,当出现问题时再找解决方案

当更新一条记录,希望没有被别人更新,通常的方式就增加一个乐观锁字段(version)即可,更新的时候把version带上,如果version不对就更新失败。

  1. 添加@version注解到字段上

    @Data
    public class User {
    	...
        @Version
        private Integer version;
    }
    
  2. 创建配置类MybatisPlusConfig ,注入bean乐观锁插件

// 千万不要忘记开启事务管理
@EnableTransactionManagement
@Configuration
public class MybatisPlusConfig {
	// 乐观锁插件
	@Bean
	public OptimisticLockerInterceptor optimisticLockerInterceptor() {
		return new OptimisticLockerInterceptor();
	}
}
  1. 测试乐观锁

    // 测试乐观锁,模拟多线程抢占的情况下是什么样子的
    @Test
    public void testOptimisticLokerFail(){
      // 查询一个数据,返回当前的一个version
      User user = userMapper.selectById(1L);
       
      // 修改数据
      user.setName("Jude1111");
      user.setAge(30);
       
      // 插入第三者
      User user2 = userMapper.selectById(1L);
      user2.setName("Jude2222");
      user2.setAge(32);
       
      // 第三者抢先执行
      userMapper.updateById(user2);
       
      // 自己再慢慢更新
      if(userMapper.updateById(user) == 1){
        System.out.println("update successfully");
      }else {
        System.out.println("update fail due to modified by others");
      }
    }
    

特别说明:

  • 支持的数据类型只有:int,Integer,long,Long,Date,Timestamp,LocalDateTime
  • 整数类型下 newVersion = oldVersion + 1
  • newVersion 会回写到 entity
  • 仅支持 updateById(id)update(entity, wrapper) 方法
  • update(entity, wrapper) 方法下, wrapper 不能复用!!!

5、逻辑删除

逻辑删除:并不是真的从数据库中删除,只是加了一个删除状态标记。

物理删除:直接从数据库中删除

使用MP实现逻辑删除:

1.添加@TableLogic注解到deleted字段上

@Data
public class User {
    @TableLogic
    private Integer deleted;
}

2.application.properties加入配置,如果配置值与mp默认的一样,可以不用配

mybatis-plus.global-config.db-config.logic-delete-value=1
mybatis-plus.global-config.db-config.logic-not-delete-value=0

3.MybatisPlusConfig 中注册逻辑删除插件

// 千万不要忘记开启事务管理
@EnableTransactionManagement
@Configuration
public class MybatisPlusConfig {
	// 乐观锁插件
  // 本质是一个拦截器 Interceptor
	@Bean
	public OptimisticLockerInterceptor optimisticLockerInterceptor() {
		return new OptimisticLockerInterceptor();
	}
  
  // 逻辑删除插件!
  @Bean
  public ISqlInjector sqlInjector() {
    return new LogicSqlInjector();
  }
}

4.测试逻辑删除

// 逻辑删除
@Test
public void testDelete(){
  int i =userMapper.deleteById(2L);
  System.out.println(i);
}

测试后发现deleted字段的值由0变成了1,且MP中的查询操作都会自动添加deleted字段的判断

@Test
public void testselect(){
  List<User> userList = userMapper.selectList(null);
}

slelect id,name,age....from user where deleted = 0

小结, 按照自动填充、逻辑删除,乐观锁的规范,以后业务表至少包含5个字段:id,create_time,update_time,deleted,version

6、分页查询

MP自带分页插件,只要简单的配置即可实现分页功能,支持多种数据库:Mysql,MariaDB,Oracle,DB2,H2,SQLServer等。

  1. MybatisPlusConfig 中注册分页插件

    // 千万不要忘记开启事务管理
    @EnableTransactionManagement
    @Configuration
    public class MybatisPlusConfig {
    	// 乐观锁插件
      // 本质是一个拦截器 Interceptor
    	@Bean
    	public OptimisticLockerInterceptor optimisticLockerInterceptor() {
    		return new OptimisticLockerInterceptor();
    	}
         
      // 逻辑删除插件!
      @Bean
      public ISqlInjector sqlInjector() {
        return new LogicSqlInjector();
      }
       
      // 分页插件
    	@Bean
    	public PaginationInterceptor paginationInterceptor() {
    		PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
    		// 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求  默认false
    		// paginationInterceptor.setOverflow(false);
    		// 设置最大单页限制数量,默认 500 条,-1 不受限制
    		// paginationInterceptor.setLimit(500);
    		return paginationInterceptor;
    	}
    }
    
  2. 测试分页

    // 分页实现  limit(sql)  PageHelper  Mp内置分页插件(导入即可!)
    @Test
    void testPage(){ // 当前页、总页数
      //1、先查询总数
      //2、本质还是 LIMIT 0,10 (默认的)
      // 参数 (当前页,每个页面的大小!)
      // 以后做分页就使用Page对象即可!
      Page<User> page = new Page<>(2,5);
      userMapper.selectPage(page,null);
       
      page.getRecords().forEach(System.out::println);	// 列表数据
      System.out.println(page.getSize());	// 每页记录数
      System.out.println(page.getTotal());	// 总记录数
      System.out.println(page.getCurrent());	// 当前页
      System.out.println(page.hasNext());		// 是否有下一页
      System.out.println(page.hasPrevious());	// 是否有上一页
    }
    

7、性能分析插件

在开发时候排查慢SQL,MybatisPlusConfig注册性能分析插件即可

/**
	 * SQL执行效率插件
	 * 3.2.0以上版本已移除,推荐使用执行sql分析打印功能
	 */
@Bean
@Profile({"dev","test"})// 设置 dev test 环境开启
public PerformanceInterceptor performanceInterceptor() {
  PerformanceInterceptor per = new PerformanceInterceptor();
  per.setMaxTime(1000); // 毫秒,超过此处设置的毫秒则sql不执行
  per.setFormat(true);    // 格式化sql
  return per;
}

3.2.0以上版本已移除,推荐使用执行sql分析打印功能,可以参考官网教程使用

测试:

将最长时间设置为1ms

// 测试性能分析插件
@Test
public void testPerformance(){
  User user = new User();
  user.setName("coding");
  user.setEmail("24736743@qq.com");
  user.setAge(18);
  userMapper.insert(user);
}

sql执行时间

抛出异常:The SQL execution time is too large

8、条件构造器

参考官方文档多使用就可以了

https://mp.baomidou.com/guide/wrapper.html#abstractwrapper

QueryWrapper

@Service
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService {

	@Autowired
	ProductMapper productMapper;

	@Override
	public void queryPage(Page<Product> pageParam,ProductQuery productQuery) {
		if(productQuery == null){
			this.page(pageParam,null);
			return;
		}

		this.page(pageParam,new QueryWrapper<Product>()				.like(!StringUtils.isEmpty(productQuery.getTitle()),"product_title",productQuery.getTitle())				.eq(!StringUtils.isEmpty(productQuery.getStatus()),"status",productQuery.getStatus())
	}
}

UpdateWrapper

@RequestMapping("/updateStatus")
public R updateStatus(@RequestBody Map<String, Object> params) {
  Object id = params.get("id");
  String status = (String) params.get("status");
  GoodsEntity goods = new GoodsEntity();
  goods.setStatus(Integer.parseInt(status));

  goodsService.update(goods,new UpdateWrapper<GoodsEntity>().eq("id",id));
  return R.ok();
}

嵌套or,and

@Test
public void testUpdate(){
  User user = new User();
  user.setAge(22);

  UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
  updateWrapper.ge("age",20)
    .and(wrapper -> wrapper.eq("name","jude").or().eq("name","jack"));
  userMapper.update(user,updateWrapper);
}

控制台打印SQL:

Time79 ms - IDcom.icoding.mapper.UserMapper.update
Execute SQL
    UPDATE
        user 
    SET
        age=22,
        update_time='2020-04-07 13:22:03.861' 
    WHERE
        del_flag=0 
        AND age >= 20 
        AND (
            name = 'jude' 
            OR name = 'jack' 
        )

注意:

插入或更新的字段有 空字符串 或者 null需求时的解决方法,都在官方文档https://mp.baomidou.com/guide/faq.html

9、baseMapper的方法注入

我们在用的时候经常就是生产自定义的Mapper继承自BaseMapper,然后我们就可以使用了,但是有没想过BaseMapper里的方法是怎么被注入到mybatis里的,怎样注入到mapper.xml文件的

MybatisPlusAutoConfiguration的SqlSessionFactory,创建bean

applyConfiguration方法

MybatisConfiguration是继承自Configuration的,自定义了一个MybatisMapperRegistry注册器,后面会用到

MybatisSqlSessionFactoryBean的初始化后方法afterPropertiesSet调用buildSqlSessionFactory创建SqlSessionFactory

点击buildSqlSessionFactory方法,里面有一段代码是会循环解析自定义mapper的xml文件的

点击parse方法

点击bindMapperForNamespace方法

其中有个addMapper的方法,是前面MybatisConfiguration调用的,点击它的addMapper方法,内部是调用MybatisMapperRegistry的方法:

/**
     * 使用自己的 MybatisMapperRegistry
     */
@Override
public <T> void addMapper(Class<T> type) {
  mybatisMapperRegistry.addMapper(type);
}

点击 mybatisMapperRegistry.addMapper(type)方法

里面会进行基本的sql方法注入

完成每个方法的注入:

注入的实现:

其实每一个AbstractMethod的子类都会实现自己的injectMappedStatement

如BaseMapper的Insert方法

DeleteByMap方法

最后会去枚举类SqlMethod中获取对应的枚举,里面就是类似定义在xml中的信息,最后转换为sqlSource再进行封装:

String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), columnScript, valuesScript);
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
return this.addInsertMappedStatement(mapperClass, modelClass, getMethod(sqlMethod), sqlSource, keyGenerator, keyProperty, keyColumn);

SqlMethod 枚举值:

最终还是调用了MapperBuilderAssistantaddMappedStatement进行注册:

如上面的DeleteByMap,注入完sql语句后的返回

return addUpdateMappedStatement(mapperClass, modelClass, getMethod(sqlMethod), sqlSource);

或者

return this.addDeleteMappedStatement(mapperClass, getMethod(sqlMethod), sqlSource);

会看抽象类AbstractMethod的源码

都调用了addMappedStatement方法

总结

  • 初始化注入自定义的MybatisConfiguration和MybatisMapperRegistry。
  • 解析Mapper类,获取方法对应的AbstractMethod。
  • 调用各自的实现进行去SqlMethod获取对应的枚举,获取到信息后进行注册。

其实就相当于代码里面定义了原本需要再xx.xml定义的数据,直接在代码中获取注入常用的CRUD操作即可

参考:https://blog.csdn.net/wangwei19871103/article/details/109771510

Post Directory