Mybatis Plus 入坑(含最新3.X配置)

简介

Mybatis-Plus(简称MP)是一个 Mybatis 的增强工具,在 Mybatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
使用它可以简化单表的操作, 节省开发时间, 国人写的文档已经非常通俗易懂了, 所以这里只是对其进行一些规范,便于多人协作开发
如果不了解mp, 请先阅读官方文档, 大约耗时半小时以内
mp最新的mybatis版本是3.4.6, mybatis-spring版本是1.3.2 比我们的全局配置稍微高了一些, 看了下相关的更新以及自测,发现是兼容的, 但是不排除有些操作不支持, 支持的操作已经足够简化开发了
现在mp最新的版本已经升级到3.X, 所以这里2.X的文档不在维护了

名词解释

GlobalConfig -> 指的是mp的全局配置文件, 版本2.X和 3.X的变化还是比较大, 可以直接看源码的注释
BaseMapper -> BaseMapper里面维护了许多常用的方法, 例如根据主键查询记录, 根据根据主键修改记录等等, 普通mapper只需要继承这个BaseMapper即可获得通用的方法
Wrapper -> 条件构造抽象类, 用来生成sql的条件
LogicSqlInjector -> 逻辑sql处理器, (逻辑删除)
PaginationInterceptor -> 分页插件, 底层是物理分页

实体类命名

GlobalConfig 中默认表名、字段名、使用下划线命名
eg. TrainCourse 对应的表名为 train_course
成员 courseType 对应表字段名 course_type
这些可以通过注解覆盖
类 : @TableName("对应的表名")
字段: @TableField("对应的字段名")


@Data
public class Course {

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

    private Integer courseType;
    private String title;

    /**
     * 城市
     */
    private String city;

	// 因为mp在使用的时候如果直接使用str, 等于就是使用了魔法值, 不便于维护, 所以统一在实体类中维护
    public static final String COURSE_ID = "course_id";
    public static final String COURSE_TYPE = "course_type";
}

  1. 如果是表中不存在的字段 , 务必打上@TableField(exist = false)
  2. 如果是表示是否删除的字段, 如deleted, 则也要打上@TableLogic 名称和全局一样, 如果和数据库字段名不符合全局的映射, 则需要手动指定
  3. 数据库中只有0,1的值, 使用Boolean类型

XML和接口

xml和接口是完全兼容之前mybatis的写法, 所以不会影响之前的逻辑。如果xml手动定义的名称和mp的BaseMapper中定义的一样, 则手动指定的会覆盖BaseMapper中的。
即顺序为 自己在xml中定义的方法 > BaseMapper中的方法
如果仅仅使用到了BaseMapper中的方法, 则可以不配置XML配置文件

使用示例

在构建好实体类后, 不要在条件查询器中直接使用魔法值, 不容易维护

// 根据主键id查询
Coursecourse = mCourseMapper.selectById(id);
 
// 使用查询器
Wrapper<Course> query = new EntityWrapper<>();
if (courseType != null) {
    // 这里的写法其实有点像jooq, 生成的sql为:   where course_type =  courseType
    query.eq(Course.COURSE_TYPE, courseType);
}
query.eq(Course.NAME, name);
// deleted 使用 true 或者 false , 尽量不要使用0,1 因为0,1是魔法值
query.eq(Course.DELETED, true);
// 查询count的数量
int result =  mCourseMapper.selectCount(query);

逻辑删除插件

一旦使用了逻辑删除插件, 以后默认的select都会带上 deleted = 0, 即只会查出不标记为删除的数据, 所以不需要手动在写

Wrapper<Test> query = new EntityWrapper<>();
query.eq(Test.DELETED, false); //这句话是多余的, 因为配置了逻辑删除插件, 默认在select的时候会加上 deleted = 0 这个条件
 
// 经过测试发现, 这会带来一个问题, 比如就是要查询删除的记录, 使用下面的这句话是无效的, 所以一旦使用了逻辑删除插件, 如果要查询删除了的记录, 请手写xml
query.eq(Test.DELETED, true); // 效果就是   WHERE deleted=0 AND (deleted = 1)  这样明显是查不出任何一条记录的

2.X版本

示例配置

mp只是用自己的 MybatisSqlSessionFactoryBean 替代了 mybatis-spring的 SqlSessionFactoryBean 其余的配置都不需要改动
再 globalConfig 中, 务必加入逻辑删除插件, 因为我们的大多数操作都是逻辑删除。物理删除请手动写xml。

3.X 版本

3.X版本和2.X版本的差别还是比较大的, 主要的差别如下
升级指南可以参考 https://my.oschina.net/u/241218/blog/1838534
其中有一点, 就是下面这个方法让我有点困惑
UpdateWrapper特有方法

方法名 说明
set SQL SET 字段(一个字段使用一次)

因为发现在使用中并没有使用到这个方法, 连BaseMapper里面的update, 根据下面的解释, 也用不到, 猜测是要自己实现一个注入方法了, 这里没有深究

配置示例

这里采用java config的方式来配置(个人喜好, 加上java config可以做一些环境判断, 上面那个打印sql每次都需要手动打开注释, 麻烦)
Spring Boot的方式非常简单, 参考官网即可, 所以这里给出的是Spring mvc的配置方式, 使用的DataSource是阿里的druid
这里就直接贴配置文件了, 下面的包含了多数据源的配置

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.core.config.GlobalConfig.DbConfig;
import com.baomidou.mybatisplus.extension.injector.LogicSqlInjector;
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.extension.plugins.PerformanceInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import java.util.ArrayList;
import java.util.List;
import org.apache.ibatis.plugin.Interceptor;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author yanghuan
 */

@Configuration
public class MybatisPlusConfig {
    
   // 环境标志, 区分dev or prod
    @Autowired
    private String projectStage;

    /**
     * 数据源a 相关信息
     */
    @Value("${a.url}")
    private String aUrl;

    @Value("${a.username}")
    private String aUsername;

    @Value("${a.password}")
    private String aPassword;

    /**
     * 数据源b 相关信息
     */
    @Value("${b.url}")
    private String bUrl;

    @Value("${b.username}")
    private String bUsername;

    @Value("${b.password}")
    private String bPassword;
   
    // 创建数据源a
    @Bean(initMethod = "init", destroyMethod = "close")
    public DruidDataSource aDataSource(){
        DruidDataSource d = new DruidDataSource();
        d.setUrl(aUrl);
        d.setUsername(aUsername);
        d.setPassword(aPassword);
        return d;
    }

    @Bean(initMethod = "init", destroyMethod = "close")
    public DruidDataSource bDataSource(){
        DruidDataSource d= new DruidDataSource();
        d.setUrl(bUrl);
        d.setUsername(bUsername);
        d.setPassword(bPassword);
        return d;
    }
  
    // 创建全局配置
    @Bean
    public GlobalConfig mpGlobalConfig() {
        // 全局配置文件
        GlobalConfig globalConfig = new GlobalConfig();
        DbConfig dbConfig = new DbConfig();
        // 默认为自增
        dbConfig.setIdType(IdType.AUTO);
        // 手动指定db 的类型, 这里是mysql
        dbConfig.setDbType(DbType.MYSQL);
        globalConfig.setDbConfig(dbConfig);
        if (!ProjectStageUtil.isProd(projectStage)) {
            // 如果是dev环境,则使用 reload xml的功能,方便调试
            globalConfig.setRefresh(true);
        }
        // 逻辑删除注入器
        LogicSqlInjector injector = new LogicSqlInjector();
        globalConfig.setSqlInjector(injector);
        return globalConfig;
    }

    @Bean(name = "aSqlSessionFactory")
    public MybatisSqlSessionFactoryBean aSqlSessionFactory(
        DruidDataSource aDataSource,
        GlobalConfig globalConfig) {
        return getSessionFactoryBean(aDataSource, globalConfig);
    }

    /**
     * MapperScannerConfigurer 是 BeanFactoryPostProcessor 的一个实现,如果配置类中出现 BeanFactoryPostProcessor ,会破坏默认的
     * post-processing, 如果不加static, 会导致整个都提前加载, 这时候, 取不到projectStage的值
     *
     * @return
     */
    @Bean
    public static MapperScannerConfigurer aMapperScannerConfigurer() {
        MapperScannerConfigurer configurer = new MapperScannerConfigurer();
        configurer.setBasePackage("com.a");
        // 设置为上面的 factory name
        configurer.setSqlSessionFactoryBeanName("bSqlSessionFactory");
        return configurer;
    }

    @Bean(name = "bSqlSessionFactory")
    public MybatisSqlSessionFactoryBean bSqlSessionFactory(
        DruidDataSource bDataSource,
        GlobalConfig mpGlobalConfig) {
        return getSessionFactoryBean(bDataSource, mpGlobalConfig);
    }

    private MybatisSqlSessionFactoryBean getSessionFactoryBean(
        aDataSource aDataSource,
        GlobalConfig globalConfig) {
        MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(aDataSource);
        sqlSessionFactoryBean.setGlobalConfig(globalConfig);
        // 源码里面如果有configuration, 不会注入BaseMapper里面的方法, 所以这里要这样写
        MybatisConfiguration configuration = new MybatisConfiguration().init(globalConfig);
        configuration.setMapUnderscoreToCamelCase(true);
        sqlSessionFactoryBean.setConfiguration(configuration);
        List<Interceptor> interceptors = new ArrayList<>();
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
        // 设置分页插件
        interceptors.add(paginationInterceptor);
        if (!ProjectStageUtil.isProd(projectStage)) {
            // 如果是dev环境,打印出sql, 设置sql拦截插件, prod环境不要使用, 会影响性能
            PerformanceInterceptor performanceInterceptor = new PerformanceInterceptor();
            interceptors.add(performanceInterceptor);
        }
        sqlSessionFactoryBean.setPlugins(interceptors.toArray(new Interceptor[0]));
        return sqlSessionFactoryBean;
    }

    /**
     * b 的mapperscan
     * @return
     */
    @Bean
    public static MapperScannerConfigurer bMapperScannerConfigurer() {
        MapperScannerConfigurer configurer = new MapperScannerConfigurer();
        configurer.setBasePackage("com.b");
        // 设置为上面的 factory name
        configurer.setSqlSessionFactoryBeanName("bSqlSessionFactory");
        return configurer;
    }
}

使用方式和之前的类似, 只不过 EntityWrapper 改为了 QueryWrapper
以及有一些新的lambda方法

  1. 不需要定义一个静态变量了, 如果在成员变量上打了@TableField注解, 这里会取注解中的值, 否则就是globalConfig里面的
@Test
public void testPluralLambda() {
    TableInfoHelper.initTableInfo(null, BdSystemUser.class);
    QueryWrapper<BdSystemUser> queryWrapper = new QueryWrapper<>();
    queryWrapper.lambda().eq(BdSystemUser::getName,"sss");
    queryWrapper.lambda().eq(BdSystemUser::getUserId,1);
    BdSystemUser user = mBdSystemUserMapper.selectOne(queryWrapper);
}
  1. 之前2.X版本对复杂的or查询不能兼容, 即使用 Wrapper 的or方法, 默认是不加括号的
    不过对于负责的查询, 还是推荐手写sql, 这里展示一下如何使用mp来构建
@Test
public void test() {
    Wrapper<BdSystemUser> wrapper = new QueryWrapper<BdSystemUser>().lambda().eq(BdSystemUser::getName, 123)
        .or(c -> c.eq(BdSystemUser::getExternalStaff, 1).eq(BdSystemUser::getUserId, 2))
        .eq(BdSystemUser::getUserId, 1);
    BdSystemUser user = mBdSystemUserMapper.selectOne(wrapper);
// 对应的sql: SELECT * FROM bdsystem_user WHERE deleted = 0 AND NAME = ? OR ( externalstaff = ? AND userid = ? ) AND userid = ?
// 可以看到, or中的语句是在括号里的
}

3.可以把where 后的条件防止在一个map中

@Test
public void testCompare() {
    Map<String, Object> map = new HashMap<>();
    map.put("userid", 1);
    map.put("ldap", "luoshu");
    QueryWrapper<BdSystemUser> queryWrapper = new QueryWrapper<BdSystemUser>()
        .allEq(true, map, false);
    BdSystemUser user = mBdSystemUserMapper.selectOne(queryWrapper);
    // SELECT userid,name,ldap,stafftype,leader,departmenttype,groupid,externalstaff,mobile,created,lastmodified,deleted FROM bdsystem_user WHERE deleted=0 AND ldap = ? AND userid = ?
	// 可以看到, map中对应的值, 是where后的条件
}

更详细的demo可以参照 demo

3.X 注意事项

  • BaseMapper的selectOne() 现在在返回多条语句的时候,会抛异常, 需要手动在条件中处理, 使用wrapper.last()方法.
  • 如果mp和tddl这样的中间件进行集成, 在分页插件的创建时候, 需要指定数据库方言, 之前可以不指定的原因是mp通过解析jdbc url的时候可以获取到对应的方言
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
 // 这边因为接了tddl, 必须手动设置方言, 否则分页会抛异常
paginationInterceptor.setDialectType(DbType.MYSQL.getDb());
posted @ 2018-09-18 14:25  hinsy  阅读(52878)  评论(0编辑  收藏  举报