MyBatisPlus


一、基本概念

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

本文制作一些要点的说明,基本的使用参见官方文档

中文文档:https://mp.baomidou.com/guide/

特性

  • 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
  • 损耗小:启动即会自动注入基本 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 操作智能分析阻断,也可自定义拦截规则,预防误操作

二、MyBatisPlus引入

1、pom

 <!--mybatis-plus-->  
<dependencies>
    <dependency>        
        <groupId>com.baomidou</groupId>        
        <artifactId>mybatis-plus-boot-starter</artifactId>                                     <version>3.0.5</version>    
    </dependency>
</dependencies>

<build>
    <resources>
        <resource>
            <directory>src/main/java</directory>
            <includes>
                <include>**/*.xml</include>
            </includes>
            <filtering>false</filtering>
        </resource>
    </resources>
</build>

2、application.yml

当springboot的版本为2.1.0以上时,MySQL配置如下

driver加个cj,url添加时区属性

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/farming_test??useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
    username: root
    password: root


mybatis-plus:
  #mybatis日志 ,用于查看sql输出日志,sql语句
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  # xml文件所在路径
  mapper-locations: classpath:/mapper/**/*.xml
  # 全局主键生成策略
  global-config:
    db-config:
      id-type: auto
      logic-delete-value: 1
      logic-not-delete-value: 0
#返回json的全局时间格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8

#配置mapper xml文件的路径
mybatis-plus.mapper-locations=classpath:com/rewind/ucenter/mapper/xml/*.xml

3、主启动类

@SpringBootApplication
// 注意MapperScan的扫描范围不能覆盖其他接口,否则MP也会误认为其他接口也是Mapper接口,也对其进行包装
@MapperScan("com.rewind.mapper")
public class GraduationProjectApplication {

    public static void main(String[] args) {
        SpringApplication.run(GraduationProjectApplication.class, args);
    }

}

4、实体类

数据库中的下划线_改为驼峰命名法

@Data
public class User {
    //order_id
    private Long orderId;
}

5、mapper

与MyBatis不同,需要继承BaseMapper

注:接口与接口之间是继承,类与接口是实现。

IDEA在 userMapper 处报错,因为找不到注入的对象,因为类是动态创建的,但是程序可以正确的执行。

只需在UserMapper加上注解@Repository

@Repository
public interface UserMapper extends BaseMapper<User> {    
}

6、service

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements DemoService {

}

三、主键策略

设置方式

在实体类的主键属性上添加注解

@TableId(type = IdType.AUTO)
private Long id;
TableId属性 类型 必须指定 默认值 描述
value String “” 主键字段名
type Enum IdType.NONE 主键类型

IdType其它主键策略:(后三个已弃用)

描述
AUTO 数据库ID自增
NONE 无状态,该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT)
INPUT insert前自行set主键值
ASSIGN_ID 分配ID(主键类型为Number(Long和Integer)或String)(since 3.3.0),使用接口IdentifierGenerator的方法nextId(默认实现类为DefaultIdentifierGenerator雪花算法)
ASSIGN_UUID 分配UUID,主键类型为String(since 3.3.0),使用接口IdentifierGenerator的方法nextUUID(默认default方法)
ID_WORKER 分布式全局唯一ID 长整型类型(please use ASSIGN_ID)
UUID 32位UUID字符串(please use ASSIGN_UUID)
ID_WORKER_STR 分布式全局唯一ID 字符串类型(please use ASSIGN_ID)

更多注解参见官网

Long 精度丢失问题

后端大 Long 对象返回前端可能导致精度丢失,当返回的 Long 对象长度超过 17 位时,就会产生,如下

后端返回:1502161733782921217(19位雪花ID)

前端接收:1502161733782921200(超出17位的部分变为0)

解决方式1:

字段加上 @JsonSerialize(using = ToStringSerializer.class) 修改 long 字段的序列化方式为字符串

import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;

@TableId(type = IdType.ASSIGN_ID)
@JsonSerialize(using = ToStringSerializer.class)
private Long id;

解决方式2(推荐):

重新注册ObjectMapper的Long类型序列化方式,推荐使用

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.Resource;
import java.math.BigInteger;

@Configuration
public class LongClassMessageConverter implements InitializingBean {

    @Resource
    ObjectMapper objectMapper;

    private SimpleModule getSimpleModule() {

        SimpleModule simpleModule = new SimpleModule();
        simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance);
        simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
        // 暂时放弃对小long的转换,约定与前端交互数据时,大Long全部转换成字符串
//        simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance);
        objectMapper.registerModule(simpleModule);
        return simpleModule;
    }


    @Override
    public void afterPropertiesSet() {
        SimpleModule simpleModule = getSimpleModule();
        objectMapper.registerModule(simpleModule);
    }
}

解决方式3(推荐):

重新构建 Jackson 序列化方式,与方式2类似的解决方式

@Configuration
public class JacksonConfig {
    /**
     * Jackson全局转化long类型为String,解决jackson序列化时传入前端Long类型缺失精度问题
     */
    @Bean
    public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
        Jackson2ObjectMapperBuilderCustomizer cunstomizer = new Jackson2ObjectMapperBuilderCustomizer() {
            @Override
            public void customize(Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder) {
                jacksonObjectMapperBuilder.serializerByType(BigInteger.class, ToStringSerializer.instance);
                jacksonObjectMapperBuilder.serializerByType(Long.class, ToStringSerializer.instance);
//                jacksonObjectMapperBuilder.serializerByType(Long.TYPE, ToStringSerializer.instance);
            }
        };
        return cunstomizer;
    }
}

以上方式针对springboot默认的Jackson序列化,fastjson等其他json组件类似处理。

四、自动填充

在添加和修改某一条数据时,对某个字段不手动赋值的话,将自动调用MyMetaObjectHandler类的方法为其赋值。

1、实体类属性注解

@Data
public class User {

    /**
     * 子节点,注解作用:表示在表中没有该字段
     */
    @TableField(exist = false)
    private List<User> children;

    //create_time
    @TableField(fill = FieldFill.INSERT)
    private Date createTime;

    //update_time
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Date updateTime;
}
//FieldFill取值
public enum FieldFill {
    /**
     * 默认不处理
     */
    DEFAULT,
    /**
     * 插入填充字段
     */
    INSERT,
    /**
     * 更新填充字段
     */
    UPDATE,
    /**
     * 插入和更新填充字段
     */
    INSERT_UPDATE
}

2、创建MyMetaObjectHandler


@Component
public class MyMetaObjectHandler implements MetaObjectHandler {

    //使用mp实现添加操作,这个方法执行
    @Override
    public void insertFill(MetaObject metaObject) {

        if( metaObject.hasSetter("createTime") ){
            //参数:实体类属性名,属性值,元数据
            this.setFieldValByName("createTime", new Date(), metaObject);
        }

        this.setFieldValByName("updateTime",new Date(),metaObject);
    }

    //使用mp实现修改操作,这个方法执行
    @Override
    public void updateFill(MetaObject metaObject) {

        this.setFieldValByName("updateTime",new Date(),metaObject);
    }
}

五、乐观锁

解决:丢失更新问题

描述:2人同时修改一条数据,后提交的覆盖先提交的,先提交的数据丢失。

例子:A,B同时修改一条数据中的工资500,A先提交500->800,B后提交500->400,期望是B提交时应为800->400。

1、悲观锁

即A在修改数据时,不允许其他人修改数据,缺点:性能较差

2、乐观锁

在表中添加字段version,所有人在修改时获得版本号,在提交时对比获得的版本号和当前数据库中的版本号是否一致,一致:则修改数据,并且版本号加一;不一致,则不可修改数据。

(1)添加version

添加注解@Version

@Data
public class User {
    private Long id;

    @Version
    @TableField(fill = FieldFill.INSERT)    //设置默认值1
    private Integer version;//版本号   
}

(2)乐观锁插件

编写配置

@Configuration
//建议将主启动类上的注解写在配置类上,避免主启动类太乱
@MapperScan("com.atguigu.mpdemo1010.mapper")
public class MpConfig {

    //乐观锁插件
    @Bean
    public OptimisticLockerInterceptor optimisticLockerInterceptor() {
        return new OptimisticLockerInterceptor();
    }
}

(3)设置默认值

@Component
public class MyMetaObjectHandler implements MetaObjectHandler {

    //使用mp实现添加操作,这个方法执行
    @Override
    public void insertFill(MetaObject metaObject) {
        //设置默认值为1
        this.setFieldValByName("version",1,metaObject);
    }
    //使用mp实现修改操作,这个方法执行
    @Override
    public void updateFill(MetaObject metaObject) {     
    }
}

六、分页查询

1、添加分页插件

@Configuration
@MapperScan("com.atguigu.mpdemo1010.mapper")
public class MpConfig {
    //分页插件
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        return new PaginationInterceptor();
    }
}

新版本如下

@Configuration
public class MybatisPlusConfig {

    /**
     * 添加分页插件
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));//如果配置多个插件,切记分页最后添加
        //interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); 如果有多数据源可以不配具体类型 否则都建议配上具体的DbType
        return interceptor;
    }
}

如果爆红需要添加下面的依赖

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-generator</artifactId>
    <version>3.4.0</version>
</dependency>

2、Page对象

1、编写分页查询的dao层接口时,传递参数 Page 即自动分页,必须放在第一位(可继承Page实现自己的分页对象)
2、分页返回的对象与传入的对象是同一个
//分页查询
@Test
public void testPage() {
    //1 创建page对象
    //传入两个参数:当前页 和 每页显示记录数
    Page<User> page = new Page<>(1,3);
    //调用mp分页查询的方法
    //调用mp分页查询过程中,底层封装
    //把分页所有数据封装到page对象里面
    userMapper.selectPage(page,null);

    //通过page对象获取分页数据
    System.out.println(page.getCurrent());//当前页
    System.out.println(page.getRecords());//每页数据list集合
    System.out.println(page.getSize());//每页显示记录数
    System.out.println(page.getTotal()); //总记录数
    System.out.println(page.getPages()); //总页数

    System.out.println(page.hasNext()); //下一页
    System.out.println(page.hasPrevious()); //上一页

}

3、自定义

service

@Override
public PageUtils listByCatId(HashMap<String,Object> params) {
    //1 创建page对象
    //传入两个参数:当前页 和 每页显示记录数
    Page<AttrGroupByCatIdVo> page = new Page<>(
        // hutool工具类,参数:map集合,map的key,key对应的value类型,如果key不存在时的默认值
        MapUtil.get(params, PageContent.PAGE,Long.class,1L),
        MapUtil.get(params, PageContent.LIMIT,Long.class,10L)
    );

    //把分页所有数据封装到page对象里面
    //分页返回的对象与传入的对象是同一个(iPage == page)
    IPage<AttrGroupByCatIdVo> iPage = baseMapper.listByCatId(page,params);

    return new PageUtils(iPage);
}

mapper

// page 分页对象,xml中可以从里面进行取值,传递参数 Page 即自动分页,必须放在第一位(你可以继承Page实现自己的分页对象)
IPage<AttrGroupByCatIdVo> listByCatId(Page<AttrGroupByCatIdVo> page, HashMap<String, Object> params);

七、逻辑删除

物理删除:真实删除,将对应数据从数据库中删除,之后查询不到此条被删除数据

逻辑删除:假删除,将对应数据中代表是否被删除字段状态修改为“被删除状态”,之后在数据库中仍 旧能看到此条数据记录

1、新增deleted字段

新增deleted字段,并添加配置

mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: flag  # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以不在实体类字段上加注解)
      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

2、插件

since 3.3.0,后可不用添加插件

@Configuration
@MapperScan("com.atguigu.mpdemo1010.mapper")
public class MpConfig {

    //逻辑删除插件
    @Bean
    public ISqlInjector sqlInjector() {
        return new LogicSqlInjector();
    }

}

3、实体类注解

@TableLogic
private Integer deleted;

4、使用

执行删除的方法即可,底层的sql调用的是update语句。

UPDATE USER SET deleted=1 WHERE id=1;

使用逻辑删除后,查询方法自动变为添加查询(自动添加WHERE deleted=0)

SELECT username,PASSWORD FROM USER WHERE deleted=0;

八、性能分析插件

性能分析拦截器,用于输出每条 SQL 语句及其执行时间
SQL 性能执行分析,开发环境使用,超过指定时间,停止运行。有助于发现问题

1、添加插件

参数:maxTime: SQL 执行最大时长,超过自动停止运行,有助于发现问题。
参数:format: SQL是否格式化,默认false

@Configuration
@MapperScan("com.atguigu.mpdemo1010.mapper")
public class MpConfig {
    /**
     * SQL 执行性能分析插件
     * 开发环境使用,线上不推荐。 maxTime 指的是 sql 最大执行时长
     *
     * 三种环境
     *      * dev:开发环境
     *      * test:测试环境
     *      * prod:生产环境
     */
    @Bean
    @Profile({"dev","test"})// 设置 dev test 环境开启
    public PerformanceInterceptor performanceInterceptor() {
        PerformanceInterceptor performanceInterceptor = new PerformanceInterceptor();
        performanceInterceptor.setMaxTime(500);//ms,超过此处设置的ms则sql不执行
        performanceInterceptor.setFormat(true);//是否格式化
        return performanceInterceptor;
    }
}

2、application

#环境设置:dev、test、prod 
spring.profiles.active=dev

九、Wrapper使用

  1. Wrapper : 条件构造抽象类,最顶端父类

  2. AbstractWrapper : 用于查询条件封装,生成 sql 的 where 条件

  3. QueryWrapper : Entity 对象封装操作类,不是用lambda语法

  4. UpdateWrapper : Update 条件封装,用于Entity对象更新操作

  5. AbstractLambdaWrapper : Lambda 语法使用 Wrapper统一处理解析 lambda 获取 column。

  6. LambdaQueryWrapper :看名称也能明白就是用于Lambda语法使用的查询Wrapper

  7. LambdaUpdateWrapper : Lambda 更新封装Wrapper

1、QueryWrapper

条件查询

//创建QueryWrapper对象
QueryWrapper<User> wrapper = new QueryWrapper<>();

//通过QueryWrapper设置条件
//ge、gt、le、lt
//查询age>=30记录
//第一个参数字段名称,第二个参数设置值
wrapper.ge("age",30);

List<User> users = userMapper.selectList(wrapper);
System.out.println(users);

2、UpdateWrapper

UserEntity user = new UserEntity();
user.setUserAge(12);

UpdateWrapper<UserEntity> wrapper = new UpdateWrapper<>();
wrapper.eq("id",1L);
// 设置 set sql语句部分,语句拼接在set后面,直接拼接,不能防止sql注入
wrapper.setSql("user_name = '王五'");

// 只会更新实体类中不为null的字段
baseMapper.update(user,wrapper);
==>  Preparing: UPDATE t_user SET user_age=?, user_name = '王五' WHERE (id = ?)
==> Parameters: 12(Integer), 1(Long)

3、LambdaWrapper

UserEntity user = new UserEntity();
//user.setUserAge(34);

LambdaUpdateWrapper<UserEntity> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(UserEntity::getId,1L)
    .set(UserEntity::getUserAge,12);

baseMapper.update(user,wrapper);
==>  Preparing: UPDATE t_user SET user_age=?, user_age=? WHERE (id = ?)
==> Parameters: 0(Integer), 12(Integer), 1(Long)

4、结合自定义sql

(1)service

QueryWrapper<UserEntity> wrapper = new QueryWrapper<>();

wrapper.eq("user_name","张三");
wrapper.orderByAsc(true,"user_age");
List<UserEntity> list =  baseMapper.findByName(wrapper);

(2)mapper

// Constants.WRAPPER 的值为 ew
List<UserEntity> findByName(@Param(Constants.WRAPPER) QueryWrapper<UserEntity> wrapper);

(3)xml

<select id="findByName" resultType="com.rewind.datasbase.entity.UserEntity">
    select * from t_user ${ew.customSqlSegment}
</select>

(5)QueryWrapper 参数

// 1、不带where的条件,xml用法${ew.sqlSegment}
// (user_name = ?) ORDER BY user_age ASC
wrapper.getSqlSegment();

// 2、带where的条件,xml用法${ew.customSqlSegment}
// WHERE (user_name = #{ew.paramNameValuePairs.MPGENVAL1}) ORDER BY user_age ASC
wrapper.getCustomSqlSegment();

// 3、where的条件的 map集合
// {MPGENVAL1=张三}
wrapper.getParamNameValuePairs();

// 4、where条件是否为空,xml用法${ew.isEmptyOfWhere}
// false
wrapper.isEmptyOfWhere();

// 5、where条件是否不为空,xml用法${ew.nonEmptyOfWhere}
// true
wrapper.nonEmptyOfWhere();

// 6、获取格式化后的执行sql
// (user_name = ?) ORDER BY user_age ASC
wrapper.getTargetSql();

wrapper.getCustomSqlSegment();

获取自定义SQL 简化自定义XML复杂情况

使用方法: select xxx from table + ${ew.customSqlSegment}

注意事项:

  1. 逻辑删除需要自己拼接条件 (之前自定义也同样)

  2. 不支持wrapper中附带实体的情况 (wrapper自带实体会更麻烦)

  3. 用法 ${ew.customSqlSegment} (不需要where标签包裹,切记!)

  4. ew是wrapper定义别名,不能使用其他的替换

(6)controller

这里写的是我在项目中遇到的接参方法,是某个项目自己封装的,并不是 MP 提供的!此处仅提供思路学习使用

前端传参

localhost:8080/list?pageNum=1&pageSize=20

body:[{
    column: "id",
    type: "eq",
    value: "123"
},{
    column: "name",
    type: "like",
    value: "张三"
}]

controller

@PostMapping("/list")
public Object getTestList(@RequestParam(name = "pageNum", required = false, defaultValue = "1") int pageNum,
                          @RequestParam(name = "pageSize", required = false, defaultValue = "20") int pageSize,
                          @RequestBody List<ConditionVo> conditionList) {
     QueryWrapper queryWrapper = SearchUtil.parseWhereSql(conditionList);
     queryWrapper.orderByDesc("CREATE_DATE");
     return service.getPageTestList(queryWrapper,pageNum,pageSize);
}

ConditionVo 条件类

@Data
public class ConditionVo implements Serializable {
    private static final long serialVersionUID = -5099378457111419832L;
    /**
     * 数据库字段名
     */
    private String column;
    /**
     * 字段值
     */
    private String value;
    /**
     * 连接类型,如llike,equals,gt,ge,lt,le
     */
    private String type;
}

parseWhereSql 方法

public static QueryWrapper parseWhereSql(List<ConditionVo> conditionList){

    for(ConditionVo conditionVo : conditionList){
        switch (conditionVo.getType()){
            case "eq": queryWrapper.eq(conditionVo.getColumn(),conditionVo.getValue());
                break;
            case "ne": queryWrapper.ne(conditionVo.getColumn(),conditionVo.getValue());
                break;
            case "like": queryWrapper.like(conditionVo.getColumn(),conditionVo.getValue());
                break;
            case "leftlike": queryWrapper.likeLeft(conditionVo.getColumn(),conditionVo.getValue());
                break;
            case "rightlike": queryWrapper.likeRight(conditionVo.getColumn(),conditionVo.getValue());
                break;
            case "notlike": queryWrapper.notLike(conditionVo.getColumn(),conditionVo.getValue());
                break;
            case "gt": queryWrapper.gt(conditionVo.getColumn(),conditionVo.getValue());
                break;
            case "lt": queryWrapper.lt(conditionVo.getColumn(),conditionVo.getValue());
                break;
            case "ge": queryWrapper.ge(conditionVo.getColumn(),conditionVo.getValue());
                break;
            case "le": queryWrapper.le(conditionVo.getColumn(),conditionVo.getValue());
                break;
        }
    }

    return queryWrapper;
}

十、枚举值处理

相信后端的同学都经历过一个情况,比如性别这个字段,分别值和名称对应1男2女,这个字段在数据库时是数值类型,而前端展示则是展示字符串的名称。有几种常见实现方案呢?

  • 数据库查询 sql 通过 case 判断,返回名称,以前 oracle 经常这么做
  • 数据库返回的值,重新遍历赋值进去,这时候还需要判断这个值到底是男是女。
  • 前端写死,返回 1 就是男,返回 2 就是女。

相信无论哪种方法都有其缺点,所以我们可以使用 mybatis-plus 提供的方式。我们在返回给前端时:

  • 只需要在遍历时 get 这个枚举,直接赋值其名称,不需要再次判断。
  • 直接返回给前端,让前端去去枚举的 name

这样大家都不需要写死这个值。

下面看看如何实现这个功能:

  • 兴义枚举,实现 IEnum 接口:
import com.baomidou.mybatisplus.annotation.IEnum;
import com.fasterxml.jackson.annotation.JsonFormat;

/**
 * @description: 性别枚举
 * @author:weirx
 * @date:2022/1/17 16:26
 * @version:3.0
 */
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum SexEnum implements IEnum<Integer> {
    MAN(1, "男"),
    WOMAN(2, "女");
    private Integer code;
    private String name;

    SexEnum(Integer code, String name) {
        this.code = code;
        this.name = name;
    }

    @Override
    public Integer getValue() {
        return code;
    }

    public String getName() {
        return name;
    }

}

@JsonFormat 注解为了解决枚举类返回前端只展示构造器名称的问题。

  • 实体类性别字段
@TableName(value = "user")
public class UserDO {

    /**
     * 主键
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    /**
     * 昵称
     */
    @TableField(value = "nickname",condition = SqlCondition.EQUAL)
    private String nickname;

    /**
     * 性别
     */
    @TableField(value = "sex")
    private SexEnum sex;

    /**
     * 版本
     */
    @TableField(value = "version",update = "%s+1")
    private Integer version;

    /**
     * 时间字段,自动添加
     */
    @TableField(value = "create_time",fill = FieldFill.INSERT)
    private LocalDateTime createTime;
}
  • 配置文件扫描枚举
mybatis-plus:
  # 支持统配符 * 或者 ; 分割
  typeEnumsPackage: com.wjbgn.*.enums
  • 定义配置文件
@Bean
public MybatisPlusPropertiesCustomizer mybatisPlusPropertiesCustomizer() {
    return properties -> {
        GlobalConfig globalConfig = properties.getGlobalConfig();
        globalConfig.setBanner(false);
        MybatisConfiguration configuration = new MybatisConfiguration();
        configuration.setDefaultEnumTypeHandler(MybatisEnumTypeHandler.class);
        properties.setConfiguration(configuration);
    };
}

序列化枚举值为数据库值

以下我是使用的 fastjson:

  • 全局(添加在前面的配置文件中):
 @Bean
 public MybatisPlusPropertiesCustomizer mybatisPlusPropertiesCustomizer() {
     // 序列化枚举值为数据库存储值
     FastJsonConfig config = new FastJsonConfig();
     config.setSerializerFeatures(SerializerFeature.WriteEnumUsingToString);

     return properties -> {
         GlobalConfig globalConfig = properties.getGlobalConfig();
         globalConfig.setBanner(false);
         MybatisConfiguration configuration = new MybatisConfiguration();
         configuration.setDefaultEnumTypeHandler(MybatisEnumTypeHandler.class);
         properties.setConfiguration(configuration);
     };
 }
  • 局部
 @JSONField(serialzeFeatures= SerializerFeature.WriteEnumUsingToString)
 private SexEnum sex;

十一、多数据源

https://baomidou.com/pages/a61e1b/#%E6%96%87%E6%A1%A3-documentation

1、依赖

目前,多数项目会有多数据源的要求,或者是主从部署的要求,所以我们还需要引入 mybatis-plus 关于多数据源的依赖:

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.0</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.27</version>
</dependency>
<!-- mybatis-plus 多数据源 -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
    <version>3.5.0</version>
</dependency>

2、配置主从

数据源配置,此处配置一主一从的环境,当前我只有一台,所以此处配置一样的:

spring:
  datasource:
    dynamic:
      primary: master #设置默认的数据源或者数据源组,默认值即为master
      strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
      datasource:
        master:
          url: jdbc:mysql://127.0.0.1:3306/rob_necessities?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone =Asia/Shanghai
          username: root
          password: 123456
        slave_1:
          url: jdbc:mysql://127.0.0.1:3306/rob_necessities?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone =Asia/Shanghai
          username: root
          password: 123456

补充 :这里面因为默认使用的是HikariCP数据源,目前也推荐使用这个,相比于druid有更高的性能,但是不能忽略下面的配置,否则服务会不断抛出异常,原因是数据库的连接时常和连接池的配置没有做好。

spring:
  datasource:
    dynamic:
      hikari:
        max-lifetime: 1800000
        connection-timeout: 5000
        idle-timeout: 3600000
        max-pool-size: 12
        min-idle: 4
        connection-test-query: /**ping*/

3、配置多主多从

前面提到过,配置文件当中配置了主从的方式,其实 mybatis-plus 还支持更多的方式:

  • 多主多从
spring:
  datasource:
    dynamic:
      primary: master #设置默认的数据源或者数据源组,默认值即为master
      strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
      datasource:
        master_1:
        master_2:
        slave_1:
        slave_2:
        slave_3:
  • 多种数据库
spring:
  datasource:
    dynamic:
      primary: mysql #设置默认的数据源或者数据源组,默认值即为master
      strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
      datasource:
        mysql:
        oracle:
        postgresql:
        h2:
        sqlserver:
  • 混合配置
spring:
  datasource:
    dynamic:
      primary: master #设置默认的数据源或者数据源组,默认值即为master
      strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
      datasource:
        master_1:
        slave_1:
        slave_2:
        oracle_1:
        oracle_2:

上面的三种方式,除了混合配置,我觉得都有肯能出现的吧。

4、使用方式

  • @DS 注解

可以注解在方法上或类上,同时存在就近原则 【方法上注解】 优先于 【类上注解】

@DS("slave_1")
public class UserServiceImpl extends ServiceImpl<UserMapper, UserDO> implements IUserService {


    @DS("salve_1")
    @Override
    public List<UserDO> getList() {
        return this.getList();
    }

    @DS("master")
    @Override
    public int saveUser(UserDO userDO) {
        boolean save = this.save(userDO);
        if (save){
            return 1;
        }else{
            return 0;
        }
    }
}

十二、更新字段为null

updateById方法,想将查询结果中某个字段原本不为null的值更新为null(数据库设计允许为null),但结果该字段更新失败,执行更新方法后改字段值未变。

问题原因

mybatis-plus FieldStrategy 有三种策略:

  • IGNORED:0 忽略
  • NOT_NULL:1 非 NULL,默认策略
  • NOT_EMPTY:2 非空

而默认更新策略是NOT_NULL:非 NULL;即通过接口更新数据时数据为NULL值时将不更新进数据库。

方案一,设置全局的field-strategy

#properties文件格式:
mybatis-plus.global-config.db-config.field-strategy=ignored
#yml文件格式:
mybatis-plus:
  global-config:
      #字段策略 0:"忽略判断",1:"非 NULL 判断",2:"非空判断"
    field-strategy: 0

这样做是全局性配置,会对所有的字段都忽略判断,如果一些字段不想要修改,但是传值的时候没有传递过来,就会被更新为null,可能会影响其他业务数据的正确性。

方案二,对某个字段设置单独的field-strategy

根据具体情况,在需要更新的字段中调整验证注解,如验证非空:

@TableField(strategy=FieldStrategy.NOT_EMPTY)

这样的话,我们只需要在需要更新为null的字段上,设置忽略策略,如下:

@TableField(strategy = FieldStrategy.IGNORED)
private LocalDateTime offlineTime;

在更新代码中,我们直接使用 mybatis-plus 中的 updateById 方法便可以更新成功

使用上述方法,如果需要这样处理的字段较多,那么就需要涉及对各个字段上都添加该注解,显得有些麻烦了。

方案三,使用UpdateWrapper更新

mybatis-plus 中,除了updateById方法,还提供了一个update方法,直接使用update方法也可以将字段设置为null,代码如下:

Article article = articleMapper.selectById(id);

LambdaUpdateWrapper<Article> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(Article::getId,article.getId());
// 将字段更新为null
updateWrapper.set(Article::getOfflineTime, null);
updateWrapper.set(Article::getContent,"try mybatis plus update null");
updateWrapper.set(Article::getPublishTime,LocalDateTime.now().plusHours(8));

int i = articleMapper.update(article, updateWrapper);

LAST、BUG 处理

1、无效列类型

MyBatis 结合 oracle 出现“无效列类型1111错误”

解决方法:配置文件新增

mybatis-plus:
    configuration:
        jdbc-type-for-null: 'null'

  目录