目录

前言:

 1.基本介绍:

2.快速入门:

2.1.环境准备

2.2.问题解决

2.3.常见注解

2.4.常见配置 

3.核心功能:

3.1.条件构造器

3.2.Service接口

4.扩展功能:

4.1.代码生成

4.2.静态工具

4.3.逻辑删除

4.4.通用枚举

4.5.JSON类型处理器

5.插件功能:

5.1.分页插件

5.2.封装分页工具


前言:

该篇文章总结了MyBatis-Plus的环境准备,使用事项,特点,核心功能,扩展功能,插件功能。

1.基本介绍:

MyBatis对于大家并不陌生,它是一个持久性框架用于对数据库进行增删改查的操作,而MyBatis-Plus本质就是对MyBatis进行一个扩展。

MyBatis-Plus的特点:

  • 润物无声:Plus只对MyBatis的基础上做增强,不会对其改变,引入它不会对现有工程产生影响(两者并不影响)
  • 效率至上:Plus只需要简单的配置,即可快速进行单表CRUD操作,节省大量时间
  • 丰富功能:可以进行代码生成,自动分页,逻辑删除,自动填充等功能

2.快速入门:

2.1.环境准备

条件:

  • 引入MaBatis-Plus对应依赖
  • 自定义Mapper接口,继承Plus提供的BaseMapper接口

1.引入依赖

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3.1</version>
</dependency>

2.自定义且继承BaseMapper接口

1.BaseMapper接口里面已经定义好了对于单表操作的一系列的增删改查的方法,如:insert(),deleteById(),updateById(),selectById()

----

2.细节:该接口使用的是泛型该定义对应方法,那么继承该接口需要指定具体类型(就是你的实体类类型)

package com.itzhanghada.mp.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itzhanghada.mp.domain.po.User;

public interface UserMapper extends BaseMapper<User> {
}

2.2.问题解决

为什么要指定泛型(具体类型)?

Mp可以通过扫描实体类,并且基于反射来获取实体类信息作为数据库的信息。

为什么可以通过实体类具体到数据库的信息呢?

通过一些约定和实体类字段名与数据库字段一致来实现

约定:

  • 类名驼峰转下划线作为表名(类名为tbUser,数据库表名为tb_user,可以对应上)
  • 类中字段名为id,直接默认为数据库中的主键字段
  • 变量名驼峰转下划线作为表中的字段名(类中字段createTime ,数据库字段creat_time,对应)

那如果不符合约定呢?

那么需要自己使用注解指定对应字段,让Mp识别

2.3.常见注解

常见注解:@TableName:指定表名,@TableId:指定主键,@TableFieId:指定字段信息

@TableName:

  • 描述:表名注解,标识实体类对应的表​
  • 使用位置:实体类
@TableName("user")
public class User {
    private Long id;
    private String name;
}

TableName注解除了指定表名以外,还可以指定很多其它属性: 

属性

类型

必须指定

默认值

描述

value

String

""

表名

schema

String

""

schema

keepGlobalPrefix

boolean

false

是否保持使用全局的 tablePrefix 的值(当全局 tablePrefix 生效时)

resultMap

String

""

xml 中 resultMap 的 id(用于满足特定类型的实体类对象绑定)

autoResultMap

boolean

false

是否自动构建 resultMap 并使用(如果设置 resultMap 则不会进行 resultMap 的自动构建与注入)

excludeProperty

String[]

{}

需要排除的属性名 @since 3.3.1

@TableId:

  • 描述:主键注解,标识实体类中的主键字段​
  • 使用位置:实体类的主键字段
@TableName("user")
public class User {
    @TableId
    private Long id;
    private String name;
}

属性

类型

必须指定

默认值

描述

value

String

""

表名

type

Enum

IdType.NONE

指定主键类型

value指定对应表名,而type指定id的生成策略(不指定,默认雪花算法生成id)

IdType(种类):

  • AuTo:自增长,每次增加一
  • INPUT:自己手动输入
  • ASSIGN_ID:雪花算法生成id

描述

AUTO

数据库 ID 自增

NONE

无状态,该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT)

INPUT

insert 前自行 set 主键值

ASSIGN_ID

分配 ID(主键类型为 Number(Long 和 Integer)或 String)(since 3.3.0),使用接口IdentifierGenerator的方法nextId(默认实现类为DefaultIdentifierGenerator雪花算法)

ASSIGN_UUID

分配 UUID,主键类型为 String(since 3.3.0),使用接口IdentifierGenerator的方法nextUUID(默认 default 方法)

 @TableFieId:

  • 描述:普通字段注解

  • 使用位置:类中字段位置

@TableName("user")
public class User {
    @TableId
    private Long id;
    private String name;
    private Integer age;
    @TableField(is_married")
    private Boolean isMarried;
    @TableField("`concat`")
    private String concat;
}

一般情况下我们并不需要给字段添加@TableField注解,一些特殊情况除外:​

  • 成员变量名与数据库字段名不一致​
  • 成员变量是以isXXX命名,按照JavaBean的规范,MybatisPlus识别字段时会把is去除,这就导致与数据库不符。​比如:isMarried => married
  • 成员变量名与数据库一致,但是与数据库的关键字冲突。使用@TableField注解给字段名添加转义字符:``。比如:order => `order`
  • 实体类有该变量,但是数据库没有该字段(不属于数据库的字段),使用exist = false 来标记它不属于数据库字段

属性

类型

必填

默认值

描述

value

String

""

数据库字段名

exist

boolean

true

是否为数据库表字段

condition

String

""

字段 where 实体查询比较条件,有值设置则按设置的值为准,没有则为默认全局的 %s=#{%s},参考(opens new window)

update

String

""

字段 update set 部分注入,例如:当在version字段上注解update="%s+1" 表示更新时会 set version=version+1 (该属性优先级高于 el 属性)

insertStrategy

Enum

FieldStrategy.DEFAULT

举例:NOT_NULL insert into table_a(<if test="columnProperty != null">column</if>) values (<if test="columnProperty != null">#{columnProperty}</if>)

updateStrategy

Enum

FieldStrategy.DEFAULT

举例:IGNORED update table_a set column=#{columnProperty}

whereStrategy

Enum

FieldStrategy.DEFAULT

举例:NOT_EMPTY where <if test="columnProperty != null and columnProperty!=''">column=#{columnProperty}</if>

fill

Enum

FieldFill.DEFAULT

字段自动填充策略

select

boolean

true

是否进行 select 查询

keepGlobalFormat

boolean

false

是否保持使用全局的 format 进行处理

jdbcType

JdbcType

JdbcType.UNDEFINED

JDBC 类型 (该默认值不代表会按照该值生效)

typeHandler

TypeHander

类型处理器 (该默认值不代表会按照该值生效)

numericScale

String

""

指定小数点后保留的位数

2.4.常见配置 

MybatisPlus也支持基于yaml文件的自定义配置,详见官方文档:使用配置 | MyBatis-Plus

大多数的配置都有默认值,因此我们都无需配置。但还有一些是没有默认值的,例如:实体类的别名扫描包​

mybatis-plus:
  type-aliases-package: com.itzhanghada.mp.domain.po
  global-config:
    db-config:
      id-type: auto # 全局id类型为自增长

注意:全局id默认id策略是雪花算法来生成id,但是如果你使用了注解指定了id生成策略,那么会优先注解(注解的优先级比全局配置高)

3.核心功能:

3.1.条件构造器

简介:支持构造各种条件来满足开发需求

类型如下:

 推荐使用Lambda开头的条件构造器:它使用了方法引用的方式,而其他条件构造器需要指定字符串(硬编码模式)(不推荐)

QueryWrapper:

查询:

@Test
void testQueryWrapper() {
    // 1.构建查询条件 where name like "%o%" AND balance >= 1000
    QueryWrapper<User> wrapper = new QueryWrapper<User>()
            .select("id", "username", "info", "balance")
            .like("username", "o")
            .ge("balance", 1000);
    // 2.查询数据
    List<User> users = userMapper.selectList(wrapper);
    users.forEach(System.out::println);
}

更新: 

@Test
void testUpdateByQueryWrapper() {
    // 1.构建查询条件 where name = "Jack"
    QueryWrapper<User> wrapper = new QueryWrapper<User>().eq("username", "Jack");
    // 2.更新数据,user中非null字段都会作为set语句
    User user = new User();
    user.setBalance(2000);
    userMapper.update(user, wrapper);
}

UpdateWrapper:

@Test
void testUpdateWrapper() {
    List<Long> ids = List.of(1L, 2L, 4L);
    // 1.生成SQL
    UpdateWrapper<User> wrapper = new UpdateWrapper<User>()
            .setSql("balance = balance - 200") // SET balance = balance - 200
            .in("id", ids); // WHERE id in (1, 2, 4)
        // 2.更新,注意第一个参数可以给null,也就是不填更新字段和数据,
    // 而是基于UpdateWrapper中的setSQL来更新
    userMapper.update(null, wrapper);
}

 LambdaQueryWrapper:

@Test
void testLambdaQueryWrapper() {
    // 1.构建条件 WHERE username LIKE "%o%" AND balance >= 1000
    LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
    wrapper
           .select(User::getId, User::getUsername, User::getInfo, User::getBalance)
           .like(User::getUsername, "o")
           .ge(User::getBalance, 1000);
    // 2.查询
    List<User> users = userMapper.selectList(wrapper);
    users.forEach(System.out::println);
}

3.2.Service接口

和继承BaseMapper一样,我们也可以继承IService,里面也实现了一系列方法:save(),remove(),update(),getById(),listById(),LambdaX(),page()

实现方式:

由于Service中经常需要定义与业务有关的自定义方法,因此我们不能直接使用IService,而是自定义Service接口,然后继承IService以拓展方法。同时,让自定义的Service实现类继承ServiceImpl,这样就不用自己实现IService中的接口了。

1.自定义接口继承IService接口(指定泛型的具体类型

2.自定义实现类实现自定义接口,并且继续ServiceImpe(指定两个参数,第一个为自定义的Mapper,实体类与上面相同)(自定义Mapper接口,实体类)

4.扩展功能:

4.1.代码生成

在使用MybatisPlus以后,基础的Mapper、Service、PO代码相对固定,重复编写也比较麻烦。因此MybatisPlus官方提供了代码生成器根据数据库表结构生成PO、Mapper、Service等相关代码。只不过代码生成器同样要编码使用,也很麻烦。 

Mp的方式:

  • 自己写对应生成代码来生成代码
  • 根据官网提供的IDEA的插件来生成代码

这里博主推荐一个更好用的插件:在IDEA的plugins市场中搜索并安装MyBatisPlus插件:

4.2.静态工具

MybatisPlus提供一个静态工具类:Db,其中的一些静态方法与IService中方法签名基本一致,也可以帮助我们实现CRUD功能

细节:由于静态方法不能指定泛型,所以需要指定字节码class(当然你传一个对象就不需要传字节码了,直接通过发射获取即可)

解决方案:Service之间相互注入会出现循环依赖问题,如果Service要相互调用可以使用Db静态工具

4.3.逻辑删除

一些重要的数据我们需要进行数据保持,那么需要进行逻辑删除

而MP提供了对应功能,无需改变方法调用,你还是可以之间调用删除方法(MP会先判断逻辑字段,最终实际上执行的是更新语句)

全局配置:

mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: deleted # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

注意: 逻辑删除本身也有自己的问题,比如:

  • 会导致数据库表垃圾数据越来越多,从而影响查询效率

  • SQL中全都需要对逻辑删除字段做判断,影响查询效率

解决方案:可以采用把删除数据迁移到其它表

4.4.通用枚举

假如我们实体类需要使用枚举字段,但是数据库对应字段是一个int类型,因此我们需要进行类型转换,而MP提供了对应的转换功能

使用方式:

  • 告诉MP,实体类中的枚举类型中的哪个字段需要转换(用@EnumValue注解标记即可)
  • 在yml中配置全局枚举处理器

举例:

mybatis-plus:
  configuration:
    default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
package com.itzhanghada.mp.enums;

import com.baomidou.mybatisplus.annotation.EnumValue;
import lombok.Getter;

@Getter
public enum UserStatus {
    NORMAL(1, "正常"),
    FREEZE(2, "冻结")
    ;
    @EnumValue
    private final int value;
    private final String desc;

    UserStatus(int value, String desc) {
        this.value = value;
        this.desc = desc;
    }
}

细节:使用枚举时,返回给前端的字段信息会是自己定义的常量名,如NORML

那么如果你需要返回枚举内部的数据(使用@JsonValue注解标记)

4.5.JSON类型处理器

如果你需要将一个对象作为字段存入数据库中,那么你需要指定对应的类型处理器

MybatisPlus提供了很多特殊类型字段的类型处理器,解决特殊字段类型与数据库类型转换的问题。例如处理JSON就可以使用JacksonTypeHandler处理器

使用方式:

  • 在要转JSON对应的实体类字段上加@TableField(typeHandler = JacksonTypeHandler.class)
  • 在类上加@TableName(autoResultMap = true) =》自动装配映射

5.插件功能:

5.1.分页插件

MybatisPlus提供了很多的插件功能,进一步拓展其功能。目前已有的插件有:

  • ​PaginationInnerInterceptor:自动分页​
  • TenantLineInnerInterceptor:多租户​
  • DynamicTableNameInnerInterceptor:动态表名​
  • OptimisticLockerInnerInterceptor:乐观锁​
  • IllegalSQLInnerInterceptor:sql 性能规范​
  • BlockAttackInnerInterceptor:防止全表更新与删除​

注意:​
使用多个分页插件的时候需要注意插件定义顺序,建议使用顺序如下:​

  • 多租户,动态表名​
  • 分页,乐观锁​
  • sql 性能规范,防止全表更新与删除​

这里介绍的是分页插件:

使用步骤:

  • 配置类中注册插件,同时添加分页插件
@Configuration​
public class MybatisConfig {​
​
    @Bean​
    public MybatisPlusInterceptor mybatisPlusInterceptor() {​
        // 初始化核心插件​
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();​
        // 添加分页插件​
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));​
        return interceptor;​
    }​
}
@Test
void testPageQuery() {
    // 1.分页查询,new Page()的两个参数分别是:页码、每页大小
    Page<User> p = userService.page(new Page<>(2, 2));
    // 2.总条数
    System.out.println("total = " + p.getTotal());
    // 3.总页数
    System.out.println("pages = " + p.getPages());
    // 4.数据
    List<User> records = p.getRecords();
    records.forEach(System.out::println);
}

这里用到了分页参数,Page,即可以支持分页参数,也可以支持排序参数。(当然你可以添加多个排序字段,会根据先后来进行排序,比如:按先年龄排,后名称排,那么只有当两者年龄相同时才会采取按名称排序)

int pageNo = 1, pageSize = 5;
// 分页参数
Page<User> page = Page.of(pageNo, pageSize);
// 排序参数, 通过OrderItem来指定
page.addOrder(new OrderItem("balance", false));

userService.page(page);

5.2.封装分页工具

@Data​
public class PageQuery {​
    private Integer pageNo;​
    private Integer pageSize;​
    private String sortBy;​
    private Boolean isAsc;​
​
    public <T>  Page<T> toMpPage(OrderItem ... orders){​
        // 1.分页条件​
        Page<T> p = Page.of(pageNo, pageSize);​
        // 2.排序条件​
        // 2.1.先看前端有没有传排序字段​
        if (sortBy != null) {​
            p.addOrder(new OrderItem(sortBy, isAsc));​
            return p;​
        }​
        // 2.2.再看有没有手动指定排序字段​
        if(orders != null){​
            p.addOrder(orders);​
        }​
        return p;​
    }​
​
    public <T> Page<T> toMpPage(String defaultSortBy, boolean isAsc){​
        return this.toMpPage(new OrderItem(defaultSortBy, isAsc));​
    }​
​
    public <T> Page<T> toMpPageDefaultSortByCreateTimeDesc() {​
        return toMpPage("create_time", false);​
    }​
​
    public <T> Page<T> toMpPageDefaultSortByUpdateTimeDesc() {​
        return toMpPage("update_time", false);​
    }​
}
@Data​
@NoArgsConstructor​
@AllArgsConstructor​
public class PageDTO<V> {​
    private Long total;​
    private Long pages;​
    private List<V> list;​
​
    /**​
     * 返回空分页结果​
     * @param p MybatisPlus的分页结果​
     * @param <V> 目标VO类型​
     * @param <P> 原始PO类型​
     * @return VO的分页对象​
     */​
    public static <V, P> PageDTO<V> empty(Page<P> p){​
        return new PageDTO<>(p.getTotal(), p.getPages(), Collections.emptyList());​
    }​
​
    /**​
     * 将MybatisPlus分页结果转为 VO分页结果​
     * @param p MybatisPlus的分页结果​
     * @param voClass 目标VO类型的字节码​
     * @param <V> 目标VO类型​
     * @param <P> 原始PO类型​
     * @return VO的分页对象​
     */​
    public static <V, P> PageDTO<V> of(Page<P> p, Class<V> voClass) {​
        // 1.非空校验​
        List<P> records = p.getRecords();​
        if (records == null || records.size() <= 0) {​
            // 无数据,返回空结果​
            return empty(p);​
        }​
        // 2.数据转换​
        List<V> vos = BeanUtil.copyToList(records, voClass);​
        // 3.封装返回​
        return new PageDTO<>(p.getTotal(), p.getPages(), vos);​
    }​
​
    /**​
     * 将MybatisPlus分页结果转为 VO分页结果,允许用户自定义PO到VO的转换方式​
     * @param p MybatisPlus的分页结果​
     * @param convertor PO到VO的转换函数​
     * @param <V> 目标VO类型​
     * @param <P> 原始PO类型​
     * @return VO的分页对象​
     */​
    public static <V, P> PageDTO<V> of(Page<P> p, Function<P, V> convertor) {​
        // 1.非空校验​
        List<P> records = p.getRecords();​
        if (records == null || records.size() <= 0) {​
            // 无数据,返回空结果​
            return empty(p);​
        }​
        // 2.数据转换​
        List<V> vos = records.stream().map(convertor).collect(Collectors.toList());​
        // 3.封装返回​
        return new PageDTO<>(p.getTotal(), p.getPages(), vos);​
    }​
}

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐