Mybatis-plus学习与实践——从繁琐的CRUD中解放出来

1. 源起

在日常的开发过程中,ORM框架是再熟悉不过的东西。在早些年的SSH时代,使用的是Hibernate,其是全自动的ORM框架,功能强大,可以完全通过代码实现sql语句的构造和查询。但这也导致其过于笨重,随着互联网技术的迭代和微服务技术概念的兴起,它的老对手Mybatis逐渐走到历史前台,其以简单,轻量化和面向sql而逐渐成为了主流的ORM框架。
虽然Mybatis使用起来灵活且轻量,但是也正由于它是半自动化的ORM框架,导致了需要编写与维护额外的文件,比如xml文件、do对象和mapper接口,这三个文件编写和维护都是需要不小成本的。
Hibernate和Mybatis的优缺点总结如下:

优点 缺点
Hibernate 1、hibernate是全自动,hibernate完全可以通过对象关系模型实现对数据库的操作,拥有完整的JavaBean对象与数据库的映射结构来自动生成sql。
2、功能强大,数据库无关性好,O/R映射能力强,需要写的代码很少,开发速度很快。
3、有更好的二级缓存机制,可以使用第三方缓存。
4、数据库移植性良好。
5、hibernate拥有完整的日志系统,hibernate日志系统非常健全,涉及广泛,包括sql记录、关系异常、优化警告、缓存提示、脏数据警告等
1、学习门槛高,精通门槛更高,程序员如何设计O/R映射,在性能和对象模型之间如何取得平衡,以及怎样用好Hibernate方面需要的经验和能力都很强才行
2、hibernate的sql很多都是自动生成的,无法直接维护sql;虽然有hql查询,但功能还是不及sql强大,见到报表等变态需求时,hql查询要虚,也就是说hql查询是有局限的;hibernate虽然也支持原生sql查询,但开发模式上却与orm不同,需要转换思维,因此使用上有些不方便。总之写sql的灵活度上hibernate不及mybatis。
Mybatis 1、易于上手和掌握,提供了数据库查询的自动对象绑定功能,而且延续了很好的SQL使用经验,对于没有那么高的对象模型要求的项目来说,相当完美。
2、sql写在xml里,便于统一管理和优化, 解除sql与程序代码的耦合。
3、提供映射标签,支持对象与数据库的orm字段关系映射
4、 提供对象关系映射标签,支持对象关系组建维护
5、提供xml标签,支持编写动态sql。
6、速度相对于Hibernate的速度较快
1、关联表多时,字段多的时候,sql工作量很大。
2、sql依赖于数据库,导致数据库移植性差。
3、由于xml里标签id必须唯一,导致DAO中方法不支持方法重载。
4、对象关系映射标签和字段映射标签仅仅是对映射关系的描述,具体实现仍然依赖于sql。
5、DAO层过于简单,对象组装的工作量较大。
6、不支持级联更新、级联删除。
7、Mybatis的日志除了基本记录功能外,其它功能薄弱很多。
8、编写动态sql时,不方便调试,尤其逻辑复杂时。
9、提供的写动态sql的xml标签功能简单,编写动态sql仍然受限,且可读性低。

由上所述, 为了提高开发效率,Mybatis官方提供了MyBatis Generator (MBG)来自动生成xml文件、mapper接口和do对象。但是不管是人工编写还是使用工具自动生成,xml文件和mapper接口的存在仍然会带来代码变更时的维护成本,那么是否有更好的解决方案呢?从这个问题出发,业界出现了一些mybatis的增强工具:比如mybatis plus,TK mybatis,其中mybatis plus是其中受众较广,功能较完善的代表。正如其官网概括地,MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。

2. Mybatis-plus

2.1 介绍

Mybatis-plus本质上是一个Mybatis的增强器,其不影响Mybatis本身的任何使用和功能,即可以正常地使用Mybatis的原本的所有能力,这也意味着在一个老项目中引入Mybatis-plus带来的影响非常小,可以没有心理负担的开始使用。其支持以下以下特性:

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

框架结构

Mybatis-plus学习与实践——从繁琐的CRUD中解放出来

2.2 核心功能

CRUD 接口

Mapper CRUD 接口

说明:

  • 通用 CRUD 封装BaseMapper接口,为 Mybatis-Plus 启动时自动解析实体表关系映射转换为 Mybatis 内部对象注入容器
  • 泛型 T 为任意实体对象
  • 参数 Serializable 为任意类型主键 Mybatis-Plus 不推荐使用复合主键约定每一张表都有自己的唯一 id 主键
  • 对象 Wrapper条件构造器
分类 方法签名 注释
保存 int insert(T entity); 插入一条记录
删除 int delete(Wrapper wrapper); 根据 entity 条件,删除记录
int deleteBatchIds(Collection<? extends Serializable> idList); 删除(根据ID 批量删除)
int deleteById(Serializable id); 根据 ID 删除
int deleteByMap(Map<String, Object> columnMap); 根据 columnMap 条件,删除记录
更新 int update(T entity, Wrapper updateWrapper); 根据 whereEntity 条件,更新记录
int updateById(T entity); 根据 ID 修改
查询 T selectById(Serializable id); 根据 ID 查询
T selectOne(Wrapper queryWrapper); 根据 entity 条件,查询一条记录
List selectBatchIds(Collection<? extends Serializable> idList); 查询(根据ID 批量查询)
List selectList(Wrapper queryWrapper); 根据 entity 条件,查询全部记录
List selectByMap(Map<String, Object> columnMap); 查询(根据 columnMap 条件)
List<Map<String, Object>> selectMaps(Wrapper queryWrapper); 根据 Wrapper 条件,查询全部记录
List selectObjs(Wrapper queryWrapper); 根据 Wrapper 条件,查询全部记录。注意: 只返回第一个字段的值
IPage selectPage(IPage page, Wrapper queryWrapper); 根据 entity 条件,查询全部记录(并翻页)
IPage<Map<String, Object>> selectMapsPage(IPage page, Wrapper queryWrapper); 根据 Wrapper 条件,查询全部记录(并翻页)
Integer selectCount(Wrapper queryWrapper); 根据 Wrapper 条件,查询总记录数

通过以上接口可看到,BaseMapper中的接口可以满足80%以上的CURD需求,这也就意味着,我们的Mapper类只需要继承BaseMapper即可,不需要添加任何代码。
比如,现在有一个user表,那么相应的会有DO和Mapper如下:

@Getter
@Setter
public class UserDO extends BaseDO {
    private String name;
    private Integer age;
    ...
}
//一行代码写完CURD
public interface UserMapper extends BaseMapper<UserDO> {}

可以看到,我们只用一行代码就实现了CURD,从此不再是CURD工程师✌️。

Service CRUD 接口

说明:

  • 通用 Service CRUD 封装IService接口,进一步封装 CRUD 采用 get 查询单行 remove 删除 list 查询集合 page 分页 前缀命名方式区分 Mapper 层避免混淆,
  • 泛型 T 为任意实体对象
  • 建议如果存在自定义通用 Service 方法的可能,请创建自己的 IBaseService 继承 Mybatis-Plus 提供的基类
  • 对象 Wrapper条件构造器
分类 方法签名 注释
保存 boolean save(T entity); 插入一条记录(选择字段,策略插入)
boolean saveBatch(Collection entityList); 插入(批量)
boolean saveBatch(Collection entityList, int batchSize); 插入(批量)
保存或更新 boolean saveOrUpdate(T entity); TableId 注解的字段的值存在则更新记录,否则插入一条记录
boolean saveOrUpdate(T entity, Wrapper updateWrapper); 根据updateWrapper尝试更新,否继续执行saveOrUpdate(T)方法
boolean saveOrUpdateBatch(Collection entityList); 批量修改插入
boolean saveOrUpdateBatch(Collection entityList, int batchSize); 批量修改插入
删除 boolean remove(Wrapper queryWrapper); 根据 entity 条件,删除记录
boolean removeById(Serializable id); 根据 ID 删除
boolean removeByMap(Map<String, Object> columnMap); 根据 columnMap 条件,删除记录
boolean removeByIds(Collection<? extends Serializable> idList); 删除(根据ID 批量删除)
更新 boolean update(Wrapper updateWrapper); 根据 UpdateWrapper 条件,更新记录 需要设置sqlset
boolean update(T entity, Wrapper updateWrapper); 根据 whereEntity 条件,更新记录
boolean updateById(T entity); 根据 ID 选择修改
boolean updateBatchById(Collection entityList); 根据ID 批量更新
boolean updateBatchById(Collection entityList, int batchSize); 根据ID 批量更新
查询单个 T getById(Serializable id); 根据 ID 查询
T getOne(Wrapper queryWrapper); 根据 Wrapper,查询一条记录。结果集,如果是多个会抛出异常,随机取一条加上限制条件 wrapper.last(“LIMIT 1”)
T getOne(Wrapper queryWrapper, boolean throwEx); 根据 Wrapper,查询一条记录
Map<String, Object> getMap(Wrapper queryWrapper); 根据 Wrapper,查询一条记录
V getObj(Wrapper queryWrapper, Function<? super Object, V> mapper); 根据 Wrapper,查询一条记录
List list(Wrapper queryWrapper); 查询列表
Collection listByIds(Collection<? extends Serializable> idList); 查询(根据ID 批量查询)
Collection listByMap(Map<String, Object> columnMap); 查询(根据 columnMap 条件)
List<Map<String, Object>> listMaps(); 查询所有列表
List<Map<String, Object>> listMaps(Wrapper queryWrapper); 查询列表
List listObjs(); 查询全部记录,注意这里默认实现中底层调用的是BaseMapper的selectObjs,所以只返回第一个字段的值
List listObjs(Function<? super Object, V> mapper); 基于转换函数查询全部记录,只返回第一个字段的值
List listObjs(Wrapper queryWrapper); 根据 Wrapper,查询全部记录,只返回第一个字段的值
List listObjs(Wrapper queryWrapper, Function<? super Object, V> mapper); 根据 Wrapper 条件,查询全部记录,只返回第一个字段的值

由以上接口可以看到,IService中封装了更多的增删改查接口。综合BaseMapper和IService两个接口来看,Mybatis-plus的开发者设计思路应该是BaseMapper中只存放最基本的CURD接口,基本都是最原子的数据库操作,而ISerivce中的接口更丰富一些,加入了更多的批量操作,且例如saveOrUpdate方法可以让上层的业务逻辑更加简洁。所以,在定义完一个继承BaseMapper的空的Mapper之后,不妨再定义一个Dao类继承IService,这样就可以完全复用Mybatis-plus提供的这些通用方法了。

//3行代码实现增强版的CURD
public interface UserDao extends IService<UserDO> {}

@Repository
public class UserDaoImpl extends ServiceImpl<UserMapper, UserDO> implements UserDAO {}

条件构造器

Mybatis-plus学习与实践——从繁琐的CRUD中解放出来

  • Wrapper : 条件构造抽象类,最顶端父类
  • AbstractWrapper : 用于查询条件封装,生成 sql 的 where 条件
  • AbstractLambdaWrapper : Lambda 语法使用 Wrapper统一处理解析 lambda 获取 column。
  • LambdaQueryWrapper :看名称也能明白就是用于Lambda语法使用的查询Wrapper
  • LambdaUpdateWrapper : Lambda 更新封装Wrapper
  • QueryWrapper : Entity 对象封装操作类,不是用lambda语法
  • UpdateWrapper : Update 条件封装,用于Entity对象更新操作

Wrapper 使用说明

函数名 说明 示例
eq 等于= eq(DO::getName, “张三”) --> name = ‘张三’
ne 不等于<> ne(DO::getName, “张三”) --> name <> ‘张三’
gt 大于> gt(DO::getAge, 18) --> age > 18
ge 大于等于>= ge(DO::getAge, 18)–> age >= 18
lt 小于< lt(DO::getAge, 18) --> age < 18
le 小于等于<= le(DO::getAge, 18) --> age <= 18
between BETWEEN 值1 AND 值2 between(“age”, 18, 30) --> age between 18 and 30
notBetween NOT BETWEEN 值1 AND 值2 notBetween(“age”, 18, 30) --> age not between 18 and 30
like LIKE ‘%值%’ like(DO::getName, “王”) --> name like ‘%王%’
notLike NOT LIKE ‘%值%’ notLike(DO::getName, “王”) --> name not like ‘%王%’
likeLeft LIKE ‘%值’ likeLeft(DO::getName, “王”) --> name like ‘%王’
likeRight LIKE ‘值%’ likeRight(DO::getName, “王”) --> name like ‘王%’
isNull 字段 IS NULL isNull(DO::getName) --> name is null
isNotNull 字段 IS NOT NULL isNotNull(DO::getName) --> name is not null
in 字段 IN (V0, V1, …) in(DO::getAge, {1,2,3}) --> age in (1,2,3)
notIn 字段 NOT IN (V0, V1, …) notIn(DO::getAge, {1,2,3}) --> age not in (1,2,3)
inSql 字段 IN (sql语句) inSql(DO:;getId, “select id from table where id < 3”) --> id in (select id from table where id < 3)
notInSql 字段 NOT IN (sql语句) notInSql(DO:;getId, “select id from table where id < 3”) --> id not in (select id from table where id < 3)
groupBy 分组:GROUP BY 字段,… groupBy(DO::getId, DO::getName) --> group by id, name
orderByAsc 排序:ORDER BY 字段, … ASC orderByAsc(DO:;getId, DO::getAge) --> order by id ASC, age ASC
orderByDesc 排序:ORDER BY 字段, … DESC orderByDesc(DO:;getId, DO::getAge) --> order by id DESC, age DESC
orderBy 排序:ORDER BY 字段, … orderBy(DO:;getId, DO::getAge) --> order by id, age
having having (sql语句) having(“sum(age) > {0}”, 10) --> having sum(age) > 10
or 拼接 OR 注意事项:注定调用or表示下一个方法不是用and连接!(不调用or则默认使用and连接)例如:eq(DO::getId, 1).or().eq(DO:;getName, “张三”)–》id = 1 or name =‘张三’
and 拼接 AND and(i->i.eq(DO::getId, 1).ne(DO::getStatus, “完成”))–> and(id = 1 and status <> ‘完成’)
apply 拼接 sql 注意事项:该方法可用于数据库函数 动态入参的params对应前面的sqlHaving内部的{index}部分,这样是不会有sql注入风险的,反之会有!例:apply(“date_format(dateColumn, ‘%Y-%m-%d’) = {0}”, “2020-07-15”) --> date_format(dateColumn, ‘%Y-%m-%d’) = ‘2020-07-15’
last 无视化优化规则直接拼接到sql的最后 只能调用一次,多次调用后以最后一次为准,有sql注入的风险,请谨慎使用。例:last(“limit 1”)
exists 拼接 EXISTS (sql语句) exists(“select id from table where age = 1”)–> exists (select id from table where age = 1)
notExists 拼接 NOT EXISTS (sql语句) notExists(“select id from table where age = 1”)–> not exists (select id from table where age = 1)
nested 正常嵌套不带 AND 或者 OR nested(i->i.eq(DO::getName, “李白”).ne(DO::getStatus, “活着”)) --> (name = ‘李白’ and status <> ‘活着’)

2.3 常用插件

分页插件

系统中对于数据量较大的核心模型表,通常会提供分页查询的功能,通常会xml文件中编写形如如下的代码:

<select id="list" resultMap="BaseResultMap">
  select
  <include refid="baseColumnList" />
  from table_a
  where status = #{status}
  <if test="start!=null">
    <if test="limit!=null">
      limit #{start},#{limit}
    </if>
  </if>
</select>

实际上对于不同的数据库分页语法可能还会有细微区别,造成了该代码的迁移性较差,那么如何不用编写就实现分页功能呢?Mybatis-plus提供了可直接使用的分页插件。

配置

    /**
     * 支持的自动分页查询插件
     * @return
     */
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
        // 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求  默认false
        paginationInterceptor.setOverflow(false);
        // 设置最大单页限制数量,默认 500 条,-1 不受限制
        paginationInterceptor.setLimit(500);
        //设置数据库类型
        paginationInterceptor.setDbType(DbType.MYSQL);

        List<ISqlParser> sqlParserList = new ArrayList<>();
		// 攻击 SQL 阻断解析器、加入解析链, 阻止恶意的全表更新删除
		//sqlParserList.add(new BlockAttackSqlParser());
        paginationInterceptor.setSqlParserList(sqlParserList);

        return paginationInterceptor;
    }

配置完毕后,将该bean设置到mybatis-plus提供的sqlSessionFactory中即可生效。

使用

@Autowired
private UserDAO userDAO;

public Ipage<UserDO> listUser(long current, long size, int age) {
	Page<UserDO> page = new Page(current, size);
    LambdaQueryWrapper<UserDO> queryWrapper = Wrappers.lambdaQuery();
    queryWrapper.eq(UserDO::getAge, age);
    IPage<UserDO> pageList = userDAO.page(page, queryWrapper);
    return pageList;
}

自动填充

正如阿里巴巴的Java开发规约中提到的,所有的表默认都需要要有id,gmt_create和gmt_modified三个字段,这造成了在数据插入和更新时,我们需要在关注这些字段的值,那么有没有优雅的方式去统一地处理这些字段呢?Mybatis-plus提供了自动填充插件。

配置

  1. 在DO中的需要自动填充的字段上加上注解
//插入时回调处理逻辑
@TableField(value = "gmt_create", fill= FieldFill.INSERT)
private Date gmtCreate;
//插入和更新时都回调处理逻辑
@TableField(value = "gmt_modified", fill=FieldFill.INSERT_UPDATE)
private Date gmtModified;
  1. 实现Mybatis-plus提供的回调接口com.baomidou.mybatisplus.core.handlers.MetaObjectHandler
/**
 * 自定义自动填充功能
 *
 * @author xx
 * @date 2020/07/17
 */
public class MybatisPlusMetaObjectHandler implements MetaObjectHandler {
    private static final String FIELD_GMT_CREATE = "gmtCreate";
    private static final String FIELD_GMT_MODIFIED = "gmtModified";
    private static final String DB_GMT_CREATE = "gmt_create";
    private static final String DB_GMT_MODIFIED = "gmt_modified";

    /**
     * 插入操作时执行自动填充方法
     * @param metaObject
     */
    @Override
    public void insertFill(MetaObject metaObject) {
        Date now = new Date();
        this.strictInsertFill(metaObject, FIELD_GMT_CREATE, Date.class, now);
        this.strictInsertFill(metaObject, FIELD_GMT_MODIFIED, Date.class, now);
    }

    /**
     * 更新时执行自动填充方法
     * @param metaObject
     */
    @Override
    public void updateFill(MetaObject metaObject) {
        Date now = new Date();
        // 3.3.2版本有bug,导致无法填充指定字段,先用下面的方法代替
        this.setFieldValByName(FIELD_GMT_MODIFIED, now, metaObject);

        //下面为官方文档用法,实际上无法更新字段,已提issue,https://github.com/baomidou/mybatis-plus/issues/2743
        //this.strictUpdateFill(metaObject, FIELD_GMT_MODIFIED, Date.class, now);
    }
}

可以看到在BaseDO中有喜闻乐见的gmt_create和gmt_modified上加上注解@TableField后即可将所有的需要自动填充字段的处理逻辑收拢在一起。

  1. 注入该处理器
    /**
     * 定义 MP 全局策略
     *
     * @return
     */
    @Bean
    public GlobalConfig globalConfig() {
        GlobalConfig globalConfig = new GlobalConfig();
        // 是否控制台 print mybatis-plus 的 LOGO
        globalConfig.setBanner(true);
        //注入自定义的自动填充处理逻辑
        globalConfig.setMetaObjectHandler(new MybatisPlusMetaObjectHandler());

        GlobalConfig.DbConfig dbConfig = new GlobalConfig.DbConfig();
        // 表名下划线命名默认true
        dbConfig.setTableUnderline(true);
        // id类型,默认为数据库自增,若为分表,则id可通过@TableId在DO中设置
        dbConfig.setIdType(IdType.AUTO);
        globalConfig.setDbConfig(dbConfig);
        
        return globalConfig;
    }

将该bean设置到mybatis-plus提供的sqlSessionFactory中即可生效,完整代码示例见本文最后提供的sqmple工程。

逻辑删除

有时为了追溯历史数据或是统计,数据库表中的数据不想进行物理删除,而是通过一个字段去做逻辑删除,这种情况下一般需要一个字段标志该数据是有效还是无效状态,同时在所有的查询和更新sql中需要带上该条件,形如:

<select id="list" resultMap="BaseResultMap">
  select
  <include refid="baseColumnList" />
  from table_a
  # 限定于查询有效状态
  where del_type = 1
</select>

如上所示,这就导致了需要重复了写很多sql或wrapper(如果用Mybatis-plus的话),这显然不够优雅,那么可以通过逻辑删除插件解决问题。

配置

  1. 在DO中加上逻辑删除的属性后,添加注解;
/**
     * 是否逻辑删除。 1有效,0已删除。 默认1
     */
@TableLogic
//@TableField(select = false)  //查询时不自动追加
private DelType delType;
  1. 设置逻辑删除的有效值和无效值
    /**
     * 定义 MP 全局策略
     *
     * @return
     */
    @Bean
    public GlobalConfig globalConfig() {
        GlobalConfig globalConfig = new GlobalConfig();
        // 是否控制台 print mybatis-plus 的 LOGO
        globalConfig.setBanner(true);
        //注入自定义的自动填充处理逻辑
        globalConfig.setMetaObjectHandler(new MybatisPlusMetaObjectHandler());
        GlobalConfig.DbConfig dbConfig = new GlobalConfig.DbConfig();

        // 表名下划线命名默认true
        dbConfig.setTableUnderline(true);
        // id类型,默认为数据库自增,若为分表,则id可通过@TableId在DO中设置
        dbConfig.setIdType(IdType.AUTO);
        // *********************逻辑已删除值********************
        dbConfig.setLogicDeleteValue(String.valueOf(DelType.DEL.getValue()));
        // *********************逻辑未删除值********************
        dbConfig.setLogicNotDeleteValue(String.valueOf(DelType.ACTIVE.getValue()));
        globalConfig.setDbConfig(dbConfig);

        return globalConfig;
    }

使用

@Autowired
private UserDAO userDAO;   

@Override
public Boolean removeByName(String name) {
    LambdaQueryWrapper<UserDO> queryWrapper = Wrappers.lambdaQuery();
    queryWrapper.eq(UserDO::getName, name);

    return userDAO.remove(queryWrapper);
}

针对该数据的删除会自动转变为更新相应的逻辑删除字段。删除后,其他所有通过Mybatis-plus执行的查询,更新都会自动追加where条件过滤掉已删除数据。
可以注意到的是,该逻辑删除字段是一个枚举字段,这个枚举是基于Mybatis-plus提供的通用枚举插件实现的,见下文。

通用枚举

Mybatis原生虽然提供了枚举支持,但是MyBatis使用EnumTypeHandler来处理enum类型的Java属性,并且存储的是enum值的名称。例如,我们有性别枚举:

/**
 * @author xx
 * @date 2020/07/20
 */
public enum GenderType{
    /**
     * 男性
     */
    MEN("男"),
    /**
     * 女性
     */
    WOMEN("女");

    private String value;

    GenderType(String value) {
        this.value = value;
    }
}

那么。实际上最后落到数据库中的是枚举的name,即字符串"MEN"和"WOMEN",对于一些比较复杂的枚举就需要自定义EnumTypeHandler的逻辑了,Mybatis-plus提供了通用插件了来帮助我们更优雅地使用枚举。

配置

使用的枚举实现Mybais-plus的接口com.baomidou.mybatisplus.core.enums.IEnum即可。

/**
 * @author xx
 * @date 2020/07/20
 */
public enum GenderType implements IEnum<String> {
    /**
     * 男性
     */
    MEN("男"),
    /**
     * 女性
     */
    WOMEN("女");

    private String value;

    GenderType(String value) {
        this.value = value;
    }

    @Override
    public String getValue() {
        return value;
    }
}

这样最后数据库中存储的就是getValue方法返回的值,同时查询时,该值会自动地映射成相应的枚举,返回的类型也可自定义。

3. 关于业务逻辑和数据逻辑隔离的思考

通过以上的方式,我们已经可以很丝滑地进行数据层相关的开发工作,那么还有进一步地改进空间吗?这要从应用分层开始说起了。实际上在一个应用中,数据层(DB表)和业务逻辑层使用的实体(DTO)通常都是有gap的,那么在业务逻辑层的使用中,常常需要频繁地进行DO和DTO的转换工作。能不能在业务逻辑层完全隔离DO呢?即为业务层提供一系列的数据服务,该服务的入参和出参都只包含DTO。为了更直观一些,下面举个小栗子。

直接在业务逻辑层使用DO

假设有两张表,一个是user表,一个是userExtention表,其中user表是主表,与userExtention表是1:1的关系。

@Autowired
private UserDao userDao;

@Autowired
private UserExtentionDao userExtentionDao;

public UserDTO queryUserInfo(String name) {
    //查询user表
    LambdaQueryWrapper<UserDO> queryWrapper = Wrappers.lambdaQuery();
    queryWrapper.eq(UserDO::getName, name);
    UserDO userDO = userDao.getOne(queryWrapper);
    //转换为DTO
    UserDTO userDTO = Converter.covert(userDO, UserDTO.class);
    
    //查询user_extention表
    LambdaQueryWrapper<UserDO> queryWrapper = Wrappers.lambdaQuery();
    queryWrapper.eq(UserDO::getName, name);
    UserExtentionDO userExtentionDO = userExtentionDao.getOne()
    //转换为DTO
    UserExtentionDTO userExtentionDTO = Converter.covert(userExtentionDO, userExtentionDO.class);
    //执行业务逻辑
    fill(userDTO, userExtentionDTO);
    return userDTO;
    
}

可以直观地看到,我们在查询到相应的DO后,需要调用转换工具转换为DTO,然后在举行具体的业务逻辑,这使得DO->DTO的转换逻辑对于所有的业务层代码是不透明的,需要手动调用一次进行转换;同时,当数据库底层结构发生变化时,需要在业务逻辑层做大量的修改,这显然不满足开闭原则,那么是不是可以设计一层通用的tableService层来隔离业务逻辑层和数据层呢?借鉴Mybatis plus的思路,有以下架构。

隔离数据层和业务层

Mybatis-plus学习与实践——从繁琐的CRUD中解放出来

即实现一个ITableService的接口,提供所有数据层的接口,同时其入参和出参都是DTO,如此所有的DO->DTO的转换将向用户透明,那么上面的例子将变为:

@Autowired
private UserTableService userTableService;

@Autowired
private UserExtentionTableService userExtentionTableService;

public UserDTO queryUserInfo(String name) {
    //查询user表
    UserDTO userDTO = userTableService.getByName(name);
    //查询user_extention表
    UserExtentionDTO userExtentionDTO = userExtentionTableService.getByName(name);
    //执行业务逻辑
    fill(userDTO, userExtentionDTO);
    return userDTO;
}

可以观察到,当我们写完空的Mapper和Dao代码后,在所有的表上肯定有一些相应高频的查询或者其他操作,比如用户表上根据姓名查询,那么可以将一些 常用的操作写在TableService中,收口所有的数据操作。这样做可以收敛所有的有关Wrapper的构造逻辑。所以,总体的思路是,Mapper是空的,Dao是空的,TableService中有少量代码。
同时,所有的转换逻辑也可以收敛到tableService中,这里我使用的转换工具是Dozer。

接口设计

![AbstractTableService.png](https://www.icode9.com/i/ll/?i=img_convert/91cb7882d00aa36e1a98558615c6fa6b.png#align=left&display=inline&height=1040&margin=[object Object]&name=AbstractTableService.png&originHeight=1040&originWidth=1544&size=65982&status=done&style=none&width=1544)

FunctionService

承载一些函数型的服务,比如统计count;

分类 方法签名 注释
统计 int count() 查询总记录数
int count(Wrapper<DO> queryWrapper) 按条件查询记录数

IWrapperService

基于Wrapper的一些接口,继承FunctionService

分类 方法签名 注释
查询列表 Page<DTO> findByPage(long current, long size,** **Wrapper<DO> queryWrapper); 翻页查询
List findListAll(Wrapper<DO> queryWrapper); 按条件查询
查询单个 DTO findOne(Wrapper<DO> queryWrapper); 查询单个
DTO findOne(Wrapper<DO> queryWrapper, boolean throwEx); 查询单个,可控制有多个result是都抛出异常

ITableService

CURD接口,继承IWrapperService

分类 方法签名 注释
保存 boolean insert(DTO dto) throws Exception 插入一条记录,失败抛异常
boolean save(DTO dto) 插入一条记录,失败返回false
查询 DTO findById(Long id); 根据id查询单个
List<DTO> findListByIds(Collection idList); 根据id列表查询
List<DTO> findListAll() 查询所有
Page<DTO> findByPage(long current, long size); 无条件翻页查询
删除 boolean removeById(Long id); 根据id删除
boolean removeByIds(Collection idList); 根据id列表删除
boolean remove(Wrapper<DO> queryWrapper); 根据条件删除
更新 boolean updateById(DTO dto); 根据id更新
boolean saveOrUpdateById(DTO dto); 存在更新记录,否插入一条记录

AbstractTableService

提供FunctionService,IWrapperService ITableService 的默认实现

public abstract class AbstractTableService<DAO extends IService<DO>, 
										   DTO extends BaseDTO, 
										   DO extends BaseDO> 
                                           implements ITableService<DTO, DO> {
	//提供FunctionService,IWrapperService ITableService 的默认实现                             
}

所以我们自身实现的相应的TableService就可以直接继承AbstractTableService,大部分能力是开箱即用的。还是以User表为例。

public interface UserTableService extends ITableService<UserDTO, UserDO> {
	//沉淀user表高频CRUD操作,将wrapper构造收拢到tableService中
    UserDTO getByName(String name);

    List<UserDTO> listByAge(Integer age);

    Boolean removeByName(String name);
}

@Service
public class UserTableserviceImpl extends AbstractTableService<UserDao, UserDTO, UserDO> implements UserTableService {

    @Override
    public UserDTO getByName(String name) {
        LambdaQueryWrapper<UserDO> queryWrapper = Wrappers.lambdaQuery();
        queryWrapper.eq(UserDO::getName, name);

        return super.findOne(queryWrapper);
    }

    @Override
    public List<UserDTO> listByAge(Integer age) {
        LambdaQueryWrapper<UserDO> queryWrapper = Wrappers.lambdaQuery();
        queryWrapper.eq(UserDO::getAge, age);

        return super.findListAll(queryWrapper);
    }

    @Override
    public Boolean removeByName(String name) {
        LambdaQueryWrapper<UserDO> queryWrapper = Wrappers.lambdaQuery();
        queryWrapper.eq(UserDO::getName, name);

        return super.remove(queryWrapper);
    }
}

4. 总结

基于Mybatis-plus和上面的tableService,可以极大地加速CRUD开发,将更多的时间和精力投入到业务路基的抽象和优化上。

参考

https://www.cnblogs.com/nov5026/p/9779242.html

Mybatis-plus官方文档

https://mybatis.plus/guide/

Mybatis example

https://blog.csdn.net/u014756578/article/details/86490052
https://www.cnblogs.com/gnz49/p/11678720.html
https://zhuanlan.zhihu.com/p/42411540

Mybatis generator

https://www.cnblogs.com/zorro-y/p/5602471.html
https://www.cnblogs.com/rain1024/p/12524552.html
https://www.cnblogs.com/andea/p/11601367.html

Mybatis 配置

https://mybatis.org/mybatis-3/zh/configuration.html

上一篇:【mybatis-plus】CRUD必备良药,mybatis的好搭档


下一篇:27. SpringBoot 切换内嵌Servlet容器