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 操作智能分析阻断,也可自定义拦截规则,预防误操作
框架结构
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 接口
说明:
分类 | 方法签名 | 注释 |
---|---|---|
保存 | 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 {}
条件构造器
- 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提供了自动填充插件。
配置
- 在DO中的需要自动填充的字段上加上注解
//插入时回调处理逻辑
@TableField(value = "gmt_create", fill= FieldFill.INSERT)
private Date gmtCreate;
//插入和更新时都回调处理逻辑
@TableField(value = "gmt_modified", fill=FieldFill.INSERT_UPDATE)
private Date gmtModified;
- 实现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后即可将所有的需要自动填充字段的处理逻辑收拢在一起。
- 注入该处理器
/**
* 定义 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的话),这显然不够优雅,那么可以通过逻辑删除插件解决问题。
配置
- 在DO中加上逻辑删除的属性后,添加注解;
/**
* 是否逻辑删除。 1有效,0已删除。 默认1
*/
@TableLogic
//@TableField(select = false) //查询时不自动追加
private DelType delType;
- 设置逻辑删除的有效值和无效值
/**
* 定义 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的思路,有以下架构。
隔离数据层和业务层
即实现一个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官方文档
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