什么是MyBatis-Plus
从名字便知它是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、SQLServer2005、SQLServer 等多种数据库
- 内置性能分析插件:可输出 Sql 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询
- 内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作
集成方法示例
- 引入依赖,使用myabtis-plus的依赖替换原有的mybatis依赖
mybatis-plus的依赖会传递依赖mybatis,所以不必再单独声明,在使用SpringBoot的项目中引入如下依赖即可。
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>the-last-version</version>
</dependency>
- 声明Sql-Mapping扫描,下面以将Sql-Mapping的接口声明放在“com.alsc.databus.order.dao”包中为例
声明扫描范围,利用MyBatis原有的@MapperScan注解
@Configuration
@MapperScan("com.alsc.databus.order.dao")
public class ModuleConfiguration {
// ... 您的Bean定义代码
// 要使用框架支持的自动分页查询,需要声明如下Bean
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
// 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认false
// paginationInterceptor.setOverflow(false);
// 设置最大单页限制数量,默认 500 条,-1 不受限制
// paginationInterceptor.setLimit(500);
return paginationInterceptor;
}
}
声明一个Mapper接口,这里需要注意该Mapper继承了MyBatis-Plus提供的BaseMapper,且泛型参数指定为要保存到数据库的实体模型类。
@Repository
public interface OrderDao extends BaseMapper<UserOrder> {
// 无须声明任何方法
}
实体模型类定义:
@Data
public class UserOrder extends Model<UserOrder> {
private Long id;
/** 外部订单id */
@TableField(updateStrategy = NEVER)
private String orderId;
/** 用户id */
private String userId;
/** 商户id */
private String merchantId;
/** saas门店id */
private Long saasStoreId;
/** 订单实付金额(包含支付级平台优惠),单位为分 */
private Long payAmount;
/** 订单来源 */
private Source source;
@TableField(fill = INSERT)
private Date gmtCreate;
@TableField(fill = INSERT_UPDATE)
private Date gmtModified;
}
-
使用Mapper接口进行数据库増删查改操作
UserOrder order = new UserOrder(); orderDao.insert(order); // 保存实体到数据表user_order,默认使用数据库自增id orderDao.delete(order.getId()); // 根据ID删除对应的记录 delete from user_order where id=1 // 更新数据实体 // update user_order set pay_amount=100, saas_store_id=534543423 where id=1 order.setPayAmount(100); order.setSaasStoreId(534543423); orderDao.updateById() // 查询列表 // select id,order_id,user_id,... from user_order where user_id='hadix' and order_id='4325542342' UserOrder query = new UserOrder(); query.setUserId("hadix"); query.setOrderId("4325542342"); List<UserOrder> orderDao.selectList(Wrappers.query(query)); // 分页查询,会自动执行以下两个sql语句 // select id,order_id,... from user_order where user_id='hadix' and order_id='4325542342' limit 0,50 // select count(*) from user_order where user_id='hadix' and order_id='4325542342' IPage<UserOrder> p = orderDao.selectPage(new Page<>(1, 50), Wrappers.query(query)); // 如果仅仅想分页查询,不关心总页数,只需要执行一个sql语句 // select id,order_id,... from user_order where user_id='hadix' and order_id='4325542342' limit 0,50 IPage<UserOrder> p = orderDao.selectPage(new Page<>(1, 50, false), Wrappers.query(query));
默认情况下生成的sql语句遵循如下约定:
- 表名 = 实体类名由驼峰式转换为下划线分隔,例如UserOrder => user_order,可以通过@TableName(name=XXX)自定义
- 列名 = 实体字段名有驼峰式转换为下划线分隔,例如userId => user_id,可以通过@TableField(name=XXX)自定义
- 主键 = 默认属性名id对应列名id为主键,可以通过@TableId(value=xxx,idType=xxx)来自定义属性对应的主键列名。idType用来自定义主键生成方式,默认为AUTO:使用数据库自增,其他可选值为INPUT:由用户输入,UUID:使用全局唯一ID等。
小结:
由上述代码示例可见,与原始MyBatis-Plus不同的地方仅有Mapper的声明需要继承BaseMapper而已,默认情况无须编写任何配置,XML文件以及SQL语句,即可获得对单个实体的基本増删查改操作,且查询语句自动支持分页。
BaseMapper声明了大量的増删查改方法,可以满足基本需求,列表如下:
/** 插入一条记录 */
int insert(T entity);
/** 根据 ID 删除 */
int deleteById(Serializable id);
/** 根据 columnMap 条件,删除记录 */
int deleteByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);
/** 根据 entity 条件,删除记录 */
int delete(@Param(Constants.WRAPPER) Wrapper<T> wrapper);
/** 删除(根据ID 批量删除) */
int deleteBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);
/** 根据 ID 修改 */
int updateById(@Param(Constants.ENTITY) T entity);
/** 根据 whereEntity 条件,更新记录 */
int update(@Param(Constants.ENTITY) T entity, @Param(Constants.WRAPPER) Wrapper<T> updateWrapper);
/** 根据 ID 查询 */
T selectById(Serializable id);
/** 查询(根据ID 批量查询) */
List<T> selectBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);
/** 查询(根据 columnMap 条件) */
List<T> selectByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);
/** 根据 entity 条件,查询一条记录 */
T selectOne(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
/** 根据 Wrapper 条件,查询总记录数 */
Integer selectCount(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
/** 根据 entity 条件,查询全部记录 */
List<T> selectList(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
/** 根据 Wrapper 条件,查询全部记录 */
List<Map<String, Object>> selectMaps(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
/** 根据 Wrapper 条件,查询全部记录 */
List<Object> selectObjs(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
/** 根据 entity 条件,查询全部记录(并翻页) */
IPage<T> selectPage(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
/** 根据 Wrapper 条件,查询全部记录(并翻页) */
IPage<Map<String, Object>> selectMapsPage(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
自定义查询、更新
BaseMapper中提供的selectByXXX系列方法接受一个queryWrapper参数,利用LambdaQueryWrapper可以类型安全的方式编写查询,避免直接编写SQL。
例如:增加一个根据订单ID查询,在上文的OrderDao接口中增加一个默认方法即可。
@Repository
public interface OrderDao extends BaseMapper<UserOrder> {
/**
* 根据订单号查询指定订单
* select id,user_id,... from user_order where order_id=? and source=?
* @param orderId 订单号
* @param source 来源
* @return 查询结果
*/
default UserOrder getByOrderId(String orderId, Source source) {
LambdaQueryWrapper<UserOrder> queryWrapper = Wrappers.lambdaQuery();
return selectOne(
queryWrapper
.eq(UserOrder::getOrderId, orderId)
.eq(UserOrder::getSource, source)
);
}
}
LambdaQueryWrapper还支持编写动态查询,例如:
@Data
public class UserRequest {
String userId;
String orderId;
}
@Repository
public interface OrderDao extends BaseMapper<UserOrder> {
/**
* 根据订单号查询指定订单
*
* <select id="find" parameterType="UserRequest" resultType="UserOrder">
* select id,user_id,... from user_order
* <where>
* <if test="userId != null">
* and user_id = #{userId}
* </if>
* <if test="orderId != null">
* and order_id = #{orderId}
* </if>
* </where>
* </select>
*
* @param orderId 订单号
* @param source 来源
* @return 查询结果
*/
default List<UserOrder> find(UserRequest req) {
LambdaQueryWrapper<UserOrder> queryWrapper = Wrappers.lambdaQuery();
return selectList(
queryWrapper
.eq(req.getUserId()!=null, UserOrder::getUserId, req.getUserId())
.eq(req.getOrderId()!=null, UserOrder::getOrderId, req.getOrderId())
);
}
/**
* 同时支持Mybatis原来使用xml定义查询的方式,另外第一个参数为IPage对象时自动支持分页
*/
List<UserOrder> findByPage(Page<UserOrder> page, @Param("req") UserRequest req);
}
<mapper namespace="OrderDao">
<select id="findByPage" parameterType="UserRequest" returnType="UserOrder">
select id,user_id,... from user_order
<where>
<if test="req.userId != null">
and user_id = #{req.userId}
</if>
<if test="req.orderId != null">
and order_id = #{req.orderId}
</if>
</where>
</select>
</mapper>
更多有关LambdaQueryWrapper的用法可以查阅官方文档
自定义更新语句:
@Repository
public interface OrderDao extends BaseMapper<UserOrder> {
/**
* 根据订单id更新订单
* <mapper namespace="OrderDao">
* <update id="updateByOrderId" parameterType="UserOrder">
* update user_order
* <set>
* <if test="userOrder.userId != null">user_id = #{userOrder.userId},</if>
* <if test="userOrder.payAmount != null">pay_amount = #{userOrder.payAmount},</if>
* <if test="userOrder.saasStoreId != null">saas_store_id = #{userOrder.getSaasStoreId}</if>
* <!-- .... 其他语句 -->
* </set>
* where order_id = #{userOrder.orderId}
* </update>
* </mapper>
*
* @param userOrder 用户订单
*/
default void updateByOrderId(UserOrder userOrder) {
LambdaUpdateWrapper<UserOrder> updateWrapper = Wrappers.lambdaUpdate();
update(userOrder, updateWrapper.eq(UserOrder::getOrderId, userOrder.getOrderId()));
}
}
更多有关LambdaUpdateWrapper的用法可以查阅官方文档
自动填充
技术团队通常对数据表结构有些规范要求,阿里的mysql规约中要求数据表必须有主键id,创建时间gmtCreated,更新时间gmtModified三个字段。业务代码中填充三个字段的代码无疑会成为样板代码,利用MyBatis-Plus提供的自动填充功能可以有效解决这个问题。
@TableField注解有个fill属性,表示被标注的字段需要自动填充,取值如下:
- INSERT:在插入时填充
- INSERT_UPDATE:在插入和更新时都更新
上面代码中对gmtCreated和gmtModified上加了@TableField(fill=INSERT)
在您的应用中添加一个类型为MetaObjectHandler类型的SpringBean
@Component
public class OrderMetaObjectHandler implements MetaObjectHandler {
private static final String FIELD_GMT_CREATE = "gmtCreate";
private static final String FIELD_GMT_MODIFIED = "gmtModified";
// 插入操作时执行自动填充方法
@Override
public void insertFill(MetaObject metaObject) {
Date now = new Date();
setInsertFieldValByName(FIELD_GMT_CREATE, now, metaObject);
setInsertFieldValByName(FIELD_GMT_MODIFIED, now, metaObject);
}
// 更新时执行自动填充方法
@Override
public void updateFill(MetaObject metaObject) {
setUpdateFieldValByName(FIELD_GMT_MODIFIED, new Date(), metaObject);
}
}
如此声明后,上述自动填充代码就会在所有的插入和更新操作时执行。
对于主键ID的自动生成,由于默认情况下主键有自己的生成策略,如果要使用自动填充,需要将主键ID的生成策略改为INPUT,可以使用@TableId(idType=INPUT)标注具体实体类的id字段或者mybatis-plus.global-config.db-config.id-type=input
进行全局配置。
下面以使用自定义序列生成主键ID为例:
@Component
public class OrderMetaObjectHandler implements MetaObjectHandler {
private static final String DEFAULT_SEQUENCE_NAME = "defaultSequence";
@Autowired
private GroupSequence seq;
@Override
public void insertFill(MetaObject metaObject) {
Class<?> tableClass = metaObject.getOriginalObject().getClass();
TableInfo tableInfo = TableInfoHelper.getTableInfo(tableClass);
setFieldValByName(tableInfo.getKeyProperty(), seq.nextValue(), metaObject);
}
@Override
public void updateFill(MetaObject metaObject) {}
}
枚举类型序列化支持
默认情况下,枚举类型将直接使用枚举值的名称保存到数据库的文本类型(char,varchar)字段中。
我们经常会使用code模式为枚举指定数字类型的编码,希望保存编码到数据库的数值类型(tinyint,int)字段中
上文中UserOrder的source字段是个枚举类型,声明如下:
public enum Source{
ELEME(1),ALIPAY(2);
@EnumValue // 表示使用该值保存到数据库
private final int code;
Source(int code){
this.code = code;
}
}
Mybatis-Plus需要通过扫描发现需要处理的枚举类,为了缩小扫描范围需要如下配置
mybatis-plus.type-enums-package=com.alsc.databus.order.model.enums
这样既可直接保存UserOrder的实体到数据库,保存和反查枚举值均能由框架自行转换,无须人工编写代码。
针对枚举的更多支持可以查阅官方文档
逻辑删除
逻辑删除是数据库操作的常用模式,通常我们会在实体类中声明一个int deleted
字段,删除时将该值设置为1。
MyBatis-Plus为我们提供了自动实现该模式的方式:
@Data
public class UserOrder{
private Long id;
@TableLogic
private Integer deleted
}
使用@TableLogic注解标注在表示逻辑删除的字段上。
然后再使用Mapper中的delete方法,实际执行的语句就是update user_order set deleted=1 where id=1
而使用Mapper的selectXXX方法,实际执行的语句是select * from user_order where id=1 and deleted =0
如果要改变表示删除状态的逻辑值,可以使用如下配置
mybatis-plus:
global-config:
db-config:
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
自动代码生成
MyBatis-Plus提供了从数据库表生成Entity,Mapper Interface,Mapper XML,Service,Controller的生成器,默认支持Velocity,Freemarker,Beetl三种模板引擎,也可以通过自行扩展支持其他的模板引擎。
感兴趣的读者可以自行查询官方文档
不鼓励使用代码生成能力,原因如下:
- 能通过模板生成的代码无疑都是样板代码
- 代码生成很难细粒度按需生成,冗余的代码会为代码重构带来不必要的负担
特别是在模型设计阶段,可能需要经常调整模型或数据表结构,这时已经生成的代码需要人工同步修改。
- 样板代码会引入大量的噪音,代码读者需要过滤噪音代码才能关注重要信息。
- 代码模板的好坏决定代码生成的质量,扩散安全风险
如果要使用代码模板,尽可能只用于生成Entity类。