知识中心

MyBatis-Plus入门-阿里云开发者社区

2020-03-09 00:00:00 mimukeji

什么是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 操作智能分析阻断,也可自定义拦截规则,预防误操作

集成方法示例

  1. 引入依赖,使用myabtis-plus的依赖替换原有的mybatis依赖

mybatis-plus的依赖会传递依赖mybatis,所以不必再单独声明,在使用SpringBoot的项目中引入如下依赖即可。

<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>mybatis-plus-boot-starter</artifactId>
  <version>the-last-version</version>
</dependency>
  1. 声明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;
}
  1. 使用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类。