MyBatis-PLus

飞书链接:https://b11et3un53m.feishu.cn/wiki/PsyawI04ei2FQykqfcPcmd7Dnsc

视频链接:https://www.bilibili.com/video/BV1Xu411A7tL?p=20&spm_id_from=pageDriver&vd_source=1a39594354c31d775ddc587407a55282

一 入门

1.1 demo

  • 引入依赖

    1
    2
    3
    4
    5
    <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3.1</version>
    </dependency>
  • Mapper 继承BaseMapper

  • 测试

1.2 常见注解

  • MP通过扫描实体类,并基于反射获取实体类信息作为数据库表信息

    • 类名驼峰转下划线作为表名
    • 名为id的字段为主键
    • 变量名驼峰转下划线作为表的字段名
  • @TableName:表名注解,标识实体类对应的表

    • ```java
      @TableName(“tb_user”)
      public class User {

      private Long id;
      private String name;
      

      }

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14

      - 两种属性

      ![image-20231018204639104](https://myl-mdimg.oss-cn-beijing.aliyuncs.com/TyporaImg/MybatisPlus.assets/image-20231018204639104.png)

      - `@TableId`:主键注解,标识实体类中的主键字段

      - ```java
      @TableName("user")
      public class User {
      @TableId("id")
      private Long id;
      private String name;
      }
    • 常见三种属性

      image-20231018204611220

  • @TableField:普通字段注解

    • ```java
      @TableName(“user”)
      public class User {
      @TableId
      private Long id;
      private String name;
      private Integer age;
      @TableField("isMarried")
      private Boolean isMarried;
      @TableField("concat")
      private String concat;
      
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25

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

      - 成员变量名与数据库字段名不一致
      - 成员变量是以`isXXX`命名,按照`JavaBean`的规范,`MybatisPlus`识别字段时会把`is`去除,这就导致与数据库不符。
      - 成员变量名与数据库一致,但是与数据库的关键字冲突。使用`@TableField`注解给字段名添加````转义

      ### 1.3 常见配置

      - yaml配置文件

      - 注意:注解的配置优先级别下面这个全局的优先级高

      - ```yaml
      mybatis-plus:
      # 实体类的别名扫描包
      type-aliases-package: com.itheima.mp.domain.po

      # Mapper.xml文件地址,当前这个是默认值。
      mapper-locations: "classpath*:/mapper/**/*.xml"

      # 全局id类型为自增长
      global-config:
      db-config:
      id-type: auto #assigh_id 雪花算法

二 核心功能

2.1 条件构造器

  • 除了新增以外,修改、删除、查询的SQL语句都需要指定where条件。因此BaseMapper中提供的相关方法除了以id作为where条件以外,还支持更加复杂的where条件。
  • image-20231018210233951
  • Wrapper为条件构造器的抽象类
    • image-20231018210359247
    • AbstractWrapper提供了where中包含的所有条件构造方法
    • QueryWrapperAbstractWrapper的基础上拓展了一个select方法,允许指定查询字段
    • UpdateWrapperAbstractWrapper的基础上拓展了一个set方法,允许指定SQL中的SET部分

2.1.1 QueryWrapper

  • ```java
    @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);
    
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    - ```Java
    @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);
    }

2.1.2 UpdateWrapper

  • ```Java
    @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);
    
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22

    #### 2.1.3 LambdaQueryWrapper

    - 无论是`QueryWrapper`还是`UpdateWrapper`在构造条件的时候都需要写死字段名称,会出现字符串`魔法值`。这在编程规范中显然是不推荐的。

    - 其中一种办法是基于变量的`gettter`方法结合反射技术。因此我们只要将条件对应的字段的`getter`方法传递给MybatisPlus,它就能计算出对应的变量名了

    - `LambdaQueryWrapper`和`LambdaUpdateWrapper`

    - ```java
    @Test
    void testLambdaQueryWrapper() {
    // 1.构建条件 WHERE username LIKE "%o%" AND balance >= 1000
    QueryWrapper<User> wrapper = new QueryWrapper<>();
    wrapper.lambda()
    .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);
    }

2.2 自定义SQL

2.2.1 基本用法

  • image-20231018212805019

    这种写法在某些企业是不允许的,因为SQL语句最好都维护在持久层,而不是业务层

  • MybatisPlus提供了自定义SQL功能,可以让我们利用Wrapper生成查询条件,再结合Mapper.xml编写SQL

  • ```java
    @Test
    void testCustomWrapper() {

    // 1.准备自定义查询条件
    List<Long> ids = List.of(1L, 2L, 4L);
    QueryWrapper<User> wrapper = new QueryWrapper<User>().in("id", ids);
    
    // 2.调用mapper的自定义方法,直接传递Wrapper
    userMapper.deductBalanceByIds(200, wrapper);
    

    }

    1
    2
    3
    4
    5
    6
    7
    8

    - 然后在UserMapper中自定义SQL

    ```java
    public interface UserMapper extends BaseMapper<User> {
    @Select("UPDATE user SET balance = balance - #{money} ${ew.customSqlSegment}")
    void deductBalanceByIds(@Param("money") int money, @Param("ew") QueryWrapper<User> wrapper);
    }

2.2.2 多表关联

  • 利用Wrapper中自定义条件结合自定义SQL来实现多表查询的效果。

  • 基于mybtis

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <select id="queryUserByIdAndAddr" resultType="com.itheima.mp.domain.po.User">
    SELECT *
    FROM user u
    INNER JOIN address a ON u.id = a.user_id
    WHERE u.id
    <foreach collection="ids" separator="," item="id" open="IN (" close=")">
    #{id}
    </foreach>
    AND a.city = #{city}
    </select>
  • 使用wrapper

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Test
    void testCustomJoinWrapper() {
    // 1.准备自定义查询条件
    QueryWrapper<User> wrapper = new QueryWrapper<User>()
    .in("u.id", List.of(1L, 2L, 4L))
    .eq("a.city", "北京");

    // 2.调用mapper的自定义方法
    List<User> users = userMapper.queryUserByWrapper(wrapper);

    users.forEach(System.out::println);
    }

2.3 Service接口

  • MybatisPlus不仅提供了BaseMapper,还提供了通用的Service接口及默认实现,封装了一些常用的service模板方法。
  • 通用接口为IService默认实现为 ServiceImpl,其中封装的方法可以分为以下几类:
    • save:新增
    • remove:删除
    • update:更新
    • get:查询单个结果
    • list:查询集合结果
    • count:计数
    • page:分页查询
  • image-20231018213623619

2.3.1 基本用法

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

  • 首先,定义IUserService,继承IService

    1
    2
    3
    public interface IUserService extends IService<User> {
    // 拓展自定义方法
    }
  • 然后,编写UserServiceImpl类,继承ServiceImpl,实现UserService

    1
    2
    3
    @Service
    public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    }

2.3.2 Lambda

  • IService中还提供了Lambda功能来简化我们的复杂查询及更新功能

  • 原来UserController中的条件查询

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @GetMapping("/list")
    @ApiOperation("根据id集合查询用户")
    public List<UserVO> queryUsers(UserQuery query){
    // 1.组织条件
    String username = query.getName();
    Integer status = query.getStatus();
    Integer minBalance = query.getMinBalance();
    Integer maxBalance = query.getMaxBalance();
    LambdaQueryWrapper<User> wrapper = new QueryWrapper<User>().lambda()
    .like(username != null, User::getUsername, username)
    .eq(status != null, User::getStatus, status)
    .ge(minBalance != null, User::getBalance, minBalance)
    .le(maxBalance != null, User::getBalance, maxBalance);
    // 2.查询用户
    List<User> users = userService.list(wrapper);
    // 3.处理vo
    return BeanUtil.copyToList(users, UserVO.class);
    }
  • Service中对LambdaQueryWrapperLambdaUpdateWrapper的用法进一步做了简化。我们无需自己通过new的方式来创建Wrapper,而是直接调用lambdaQuerylambdaUpdate方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @GetMapping("/list")
    @ApiOperation("根据id集合查询用户")
    public List<UserVO> queryUsers(UserQuery query){
    // 1.组织条件
    String username = query.getName();
    Integer status = query.getStatus();
    Integer minBalance = query.getMinBalance();
    Integer maxBalance = query.getMaxBalance();
    // 2.查询用户
    List<User> users = userService.lambdaQuery()
    .like(username != null, User::getUsername, username)
    .eq(status != null, User::getStatus, status)
    .ge(minBalance != null, User::getBalance, minBalance)
    .le(maxBalance != null, User::getBalance, maxBalance)
    .list();
    //最后的.one()返回最多一个结果,.list()返回集合, .count()返回计数结果

    // 3.处理vo
    return BeanUtil.copyToList(users, UserVO.class);
    }
  • 需求新增:如果扣减后余额为0,则将用户status修改为冻结状态(2

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    @Override
    @Transactional
    public void deductBalance(Long id, Integer money) {
    // 1.查询用户
    User user = getById(id);
    // 2.校验用户状态
    if (user == null || user.getStatus() == 2) {
    throw new RuntimeException("用户状态异常!");
    }
    // 3.校验余额是否充足
    if (user.getBalance() < money) {
    throw new RuntimeException("用户余额不足!");
    }
    // 4.扣减余额 update tb_user set balance = balance - ?
    int remainBalance = user.getBalance() - money;
    lambdaUpdate()
    .set(User::getBalance, remainBalance) // 更新余额
    .set(remainBalance == 0, User::getStatus, 2) // 动态判断,是否更新status
    .eq(User::getId, id)
    .eq(User::getBalance, user.getBalance()) // 乐观锁
    .update();
    }

2.3.4 批量新增

  • 比for循环一个一个插入快很多

  • MybatisPlus的批处理是基于PrepareStatement的预编译模式,然后批量提交,最终在数据库执行时还是会有多条insert语句,逐条插入数据。

  • 修改项目中的application.yml文件,在jdbc的url后面添加参数&rewriteBatchedStatements=true:

    1
    2
    3
    4
    5
    6
    spring:
    datasource:
    url: jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: MySQL123
  • 性能将进一步提升

三 扩展功能

3.1 代码生成器

  • idea安装MyBatisPlus插件,按步骤填写参数即可

3.2 静态工具

  • 静态工具类 Db
    • 一些静态方法与Iservice中的方法签名基本一致

3.3 逻辑删除

  • 删除的时候并没有真正的删除

  • 步骤

    • 在实体类中添加一个deleted字段

    • application.yml中配置逻辑删除字段:

      1
      2
      3
      4
      5
      6
      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)
    • 执行删除操作

      1
      2
      3
      4
      5
      @Test
      void testDeleteByLogic() {
      // 删除方法与以前没有区别
      addressService.removeById(59L);
      }

      image-20231018222956813

3.4 通用枚举

  • 我们一般会定义一个枚举,做业务判断的时候就可以直接基于枚举做比较。但是我们数据库采用的是int类型,对应的PO也是Integer。因此业务操作时必须手动把枚举Integer转换,非常麻烦。

  • 解决

    • 定义枚举

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      @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;
      }
      }
    • 使用 @EnumValue标记枚举属性的值

    • 配置枚举处理器(在yaml文件中)

      1
      2
      3
      mybatis-plus:
      configuration:
      default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler

3.4 JSON类型处理器

  • 数据表中有一个类型为json类型的字段info的时候

  • 步骤

    • 定义实体UserInfo对应那个info对象

    • 使用类型处理器

      1
      2
      @TableField(typeHandler = JacksonTypeHandler.class)
      private UserInfo info;
    • 在类上开启resultMap自动转换

      1
      2
      3
      4
      5
      6
      7
      8
      9
      @Data
      @TableName(Value="user", autoResultMap = true)
      public class User {
      private Long id;
      private String name;

      @TableField(typeHandler = JacksonTypeHandler.class)
      private UserInfo info;
      }

四 插件功能

4.1 分页插件

  • 新建配置类,在其中定一个bean

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Configuration
    public class MybatisConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
    // 初始化核心插件
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    // 添加分页插件
    interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
    // 还可以继续添加其他插件
    return interceptor;
    }
    }
  • 分页API

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @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的常见API

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

    userService.page(page);

4.2 通用分页实体

4.2.1 比较规范的用户分页查询接口

  • 请求方式:GET

  • 请求路径:/users/page

  • 请求参数:

    1
    2
    3
    4
    5
    6
    7
    8
    {
    "pageNo": 1,
    "pageSize": 5,
    "sortBy": "balance",
    "isAsc": false,
    "name": "o",
    "status": 1
    }
  • 返回值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    {
    "total": 100006,
    "pages": 50003,
    "list": [
    {
    "id": 1685100878975279298,
    "username": "user_9****",
    "info": {
    "age": 24,
    "intro": "英文老师",
    "gender": "female"
    },
    "status": "正常",
    "balance": 2000
    }
    ]
    }

4.2.2 步骤

  • 定义三个实体

    • UserQuery:分页查询条件的实体,包含分页、排序参数、过滤条件

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      @EqualsAndHashCode(callSuper = true)
      @Data
      @ApiModel(description = "用户查询条件实体")
      public class UserQuery extends PageQuery {
      @ApiModelProperty("用户名关键字")
      private String name;
      @ApiModelProperty("用户状态:1-正常,2-冻结")
      private Integer status;
      @ApiModelProperty("余额最小值")
      private Integer minBalance;
      @ApiModelProperty("余额最大值")
      private Integer maxBalance;
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      @Data
      @ApiModel(description = "分页查询实体")
      public class PageQuery {
      @ApiModelProperty("页码")
      private Integer pageNo;
      @ApiModelProperty("页码")
      private Integer pageSize;
      @ApiModelProperty("排序字段")
      private String sortBy;
      @ApiModelProperty("是否升序")
      private Boolean isAsc;
      }
    • PageDTO:分页结果实体,包含总条数、总页数、当前页数据

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      @Data
      @ApiModel(description = "分页结果")
      public class PageDTO<T> {
      @ApiModelProperty("总条数")
      private Long total;
      @ApiModelProperty("总页数")
      private Long pages;
      @ApiModelProperty("集合")
      private List<T> list;
      }
    • UserVO:用户页面视图实体

  • 开发接口:在UserController中定义分页查询用户的接口