MyBatis-Plus 开发实战:常用功能与最佳实践

207 阅读20分钟

MyBatis-Plus

一、介绍

MyBatis-Plus(简称 MP)是一个基于 MyBatis 的增强工具,它简化了 MyBatis 的开发过程,提供了许多自动化的功能,减少了开发者的工作量。MyBatis-Plus 主要提供了对 MyBatis 的扩展,增加了很多功能,例如自动生成 SQL、分页查询、性能分析等。

主要特点:

1.简化 MyBatis 的操作:
  • 单表操作:MyBatis-Plus 提供了通用的 BaseMapper,可以大大简化增删改查操作。例如,使用 BaseMapper 提供的默认方法,可以直接进行插入、删除、更新和查询等操作,而不需要自己写 SQL。
  • 减少冗余代码:只需要简单地继承 BaseMapper 接口,就可以完成对数据库表的操作,避免了手写大量的 CRUD 代码。
2.自动生成 SQL
  • 条件构造器:MyBatis-Plus 提供了丰富的 Wrapper类(如 QueryWrapper和 UpdateWrapper),可以通过构造器模式灵活地构建 SQL 查询和更新条件,避免手写 SQL 语句。
  • SQL 注入器:MyBatis-Plus 采用了 SQL 注入器机制,使得开发者可以在不写 SQL 的情况下,完成对数据库的常规操作。
3.分页功能
  • MyBatis-Plus 集成了分页插件,支持基于分页参数的 SQL 查询,无需额外配置,只需要简单使用。
4.性能分析
  • MyBatis-Plus 提供了 SQL 性能分析插件,可以帮助开发者分析 SQL 执行的性能,避免不合理的查询。
5.代码生成器
  • MyBatis-Plus 提供了代码生成器,能够自动根据数据库表生成对应的 Java 实体类、Mapper 接口、XML 文件等,减少手动编写代码的工作量。
6.逻辑删除
  • MyBatis-Plus 支持逻辑删除,通过配置字段,可以将记录标记为已删除,而不是物理删除。
7.多种自定义功能
  • 自定义 SQL 方法:MyBatis-Plus 允许开发者根据需要自定义 SQL 操作,提供更大的灵活性。
  • 支持全局配置:开发者可以配置全局的规则,比如字段名自动驼峰转化、主键策略等。

主要功能和优势:

1.简化开发

MyBatis-Plus 抽象了大量重复的操作,让开发者只关注业务逻辑。

2.增强 MyBatis 功能

MyBatis 本身是一个很强大的持久层框架,而 MyBatis-Plus 通过提供许多增强功能,让 MyBatis 更加易用。

3.提高开发效率

提供了自动化生成代码、分页查询、条件构造器等功能,可以极大提高开发效率。

4.兼容性强

MyBatis-Plus 保持与 MyBatis 的高度兼容,可以无缝集成到现有的 MyBatis 项目中。

二、快速使用

1.引入依赖

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

2.自定义Mapper基础BaseMapper

image.png

3.在实体类上添加注解声明 表信息

image.png

4.在application.yml中根据需要添加配置

mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.itheima.mp.domain.po
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开启日志
    map-underscore-to-camel-case: true # 开启驼峰命名转换
    cache-enabled: false # 禁用二级缓存
  global-config:
    db-config:
      id-type: auto # 主键自增
      update-strategy: not_null # 只更新非空字段

5.注入service测试

image.png

三、常用注解

1. @TableName

  • 用于指定实体类与数据库表的映射关系。
  • 如果实体类的名称与数据库表名称不同,可以使用此注解进行映射。
@TableName("tb_user")
public class User {
    // 属性...
}

2. @TableId

  • 用于标记实体类中的主键字段,通常是与数据库表的主键列对应。
  • @TableId支持主键的自增、雪花算法等策略。
@TableId(value = "id", type = IdType.AUTO)
private Long id;

3. @TableField

  • 用于指定实体类字段与数据库表字段的映射关系。
  • 可以指定一些额外的映射特性,如是否插入、是否更新等。
@TableField(value = "user_name", exist = true)
private String userName;

4. @Version

  • 用于标记乐观锁字段。
  • 通过该字段进行并发控制,防止多个线程同时修改同一条记录导致的数据冲突。
@Version
private Integer version;

5. @TableLogic

  • 用于标记逻辑删除字段,MyBatis-Plus 会自动根据该字段的值来处理删除操作。
  • 通常该字段为 deleted,值为 1 表示已删除,0 表示未删除。
@TableLogic
private Integer deleted;

6. @TableField(exist = false)

  • 用于表示该字段不是数据库表中的字段,避免 MyBatis-Plus 尝试映射到数据库表。
  • 例如,某些属性是计算出来的字段,或者不希望进行数据库操作的字段。
@TableField(exist = false)
private String tempData;

这些注解是 MyBatis-Plus 提供的一些常见注解,通过这些注解,可以简化实体类的定义和数据库操作的配置。

四、常用配置

1. 基本配置

mybatis-plus:
  global-config:
    db-config:
      # 设置数据库类型
      database-type: MYSQL
      # 主键策略
      id-type: auto
      # 逻辑删除的字段值
      logic-delete-value: 1
      logic-not-delete-value: 0
      # 是否开启AR模式(ActiveRecord模式)
      active-record: false
      # 字段策略,未配置则采用默认
      field-strategy: not_empty
      # 是否开启全局缓存
      enable-cache: false

2. 分页插件配置

MyBatis-Plus 内置了分页插件,可以在 application.yaml 中配置分页插件的相关参数。

mybatis-plus:
  configuration:
    # 开启分页插件
    pagination-interceptor:
      # 设置最大查询条数
      limit: 500
      # 是否允许查询超出最大页码的数据,默认false
      overflow: false

3. SQL 注入配置

MyBatis-Plus 可以根据不同的数据库类型自动配置适配器。通过 database-type 可以指定具体的数据库类型。

mybatis-plus:
  global-config:
    db-config:
      # 设置数据库类型(MySQL、PostgreSQL、Oracle 等)
      database-type: MYSQL
      sql-injector: com.baomidou.mybatisplus.core.injector.DefaultSqlInjector

4. 自动填充配置

通过 MyBatis-Plus 的自动填充功能,可以配置全局的填充策略,例如创建时间和更新时间等字段的自动填充。

mybatis-plus:
  global-config:
    db-config:
      # 设置自动填充字段(比如插入时填充创建时间,更新时填充更新时间)
      fill-strategy: insert,update

5. 多数据源配置

如果项目中有多个数据源,可以在 application.yaml 中配置多数据源,并通过 @Primary@Qualifier 注解进行切换。

spring:
  datasource:
    dynamic:
      primary: master
      datasource:
        master:
          url: jdbc:mysql://localhost:3306/master_db
          username: root
          password: password
        slave:
          url: jdbc:mysql://localhost:3306/slave_db
          username: root
          password: password

6. 日志打印配置

MyBatis-Plus 可以配置 SQL 执行日志的打印。通过设置日志级别为 debug 可以打印出执行的 SQL 语句。

logging:
  level:
    com.baomidou.mybatisplus: DEBUG

7. 表前缀和字段命名策略

可以配置全局表前缀和字段命名策略,以便统一处理数据库表和实体类之间的映射关系。

mybatis-plus:
  global-config:
    db-config:
      # 表前缀
      table-prefix: mp_
      # 字段命名策略
      field-strategy: not_empty
      # 表字段驼峰命名规则
      name-style: camel

8. SQL 注入器和自定义 SQL 策略

MyBatis-Plus 支持自定义 SQL 注入器,用来实现更复杂的 SQL 生成逻辑。可以在 application.yaml 中配置。

mybatis-plus:
  global-config:
    sql-injector:
      # 可以设置为自定义的 SQL 注入器
      custom-sql-injector: com.example.CustomSqlInjector

9. MyBatis 配置

如果需要对 MyBatis 的其他配置项进行调整(例如缓存、延迟加载等),可以在 application.yaml 中进行配置。

mybatis-plus:
  configuration:
    # 是否启用缓存
    cache-enabled: true
    # 是否启用懒加载
    lazy-load-enabled: true

10. 其他常用配置

  • 实体类扫描路径:
mybatis-plus:
  type-aliases-package: com.example.domain
  • Mapper.xml 文件扫描路径:
mybatis-plus:
  mapper-locations: classpath:mapper/**/*Mapper.xml

五、核心功能

1.条件构造器

基于QueryWrapper的查询

 @Test
    void testQueryWrapper() {
        //需求:查询出名字中带a的,存款大于等于1000元的人的id、username、info、balance字段
        QueryWrapper<User> userQueryWrapper = new QueryWrapper<User>()
                .select("id","username","info","balance")
                .like("username","a")
                .ge("balance",1000);
        userMapper.selectList(userQueryWrapper).forEach(System.out::println);
    }

基于QueryWrapper的更新

  @Test
    void testUpdateQueryWrapper() {
        //需求:将用户名为jack的薪资调整为100000000
        QueryWrapper<User> queryWrapper = new QueryWrapper<User>()
                .eq("username", "jack");
        User user = new User();
        user.setBalance(100000000);
        userMapper.update(user, queryWrapper);
    }

基于UpdateWrapper的更新

 @Test
    void testUpdateWrapper() {
        //需求:更新id为1,2,4的用户的余额,加100
        List<Long> idList = List.of(1L, 2L, 4L);
        UpdateWrapper<User> updateWrapper = new UpdateWrapper<User>()
                .setSql("balance = balance + 100")
                .in("id", idList);
        userMapper.update(null, updateWrapper);
    }

使用LamdaWrapper

@Test
    void testLambdaQueryWrapper() {
        //可以使用QueryWrapper.lambda()方式
        LambdaQueryWrapper<User> lambdaQueryWrapper = new QueryWrapper<User>().lambda()
                .select(User::getId, User::getUsername, User::getInfo, User::getBalance)
                .eq(User::getUsername, "jack");
        //也可以直接使用LambdaQueryWrapper方式
        LambdaQueryWrapper<User> userLambdaQueryWrapper =  new LambdaQueryWrapper<User>()
                .eq(User::getUsername, "jack");
​
        userMapper.selectList(lambdaQueryWrapper).forEach(System.out::println);
        userMapper.selectList(userLambdaQueryWrapper).forEach(System.out::println);
    }

总结

条件构造器的用法:

•QueryWrapper和LambdaQueryWrapper通常用来构建select、delete、update的where条件部分

•UpdateWrapper和LambdaUpdateWrapper通常只有在set语句比较特殊才使用

•尽量使用LambdaQueryWrapper和LambdaUpdateWrapper,避免硬编码

2.自定义SQL

我们可以利用MyBatisPlus的Wrapper来构建复杂的Where条件,然后自己定义SQL语句中剩下的部分。

①基于Wrapper构建where条件

List<Long> ids = List.of(1L, 2L, 4L);
int amount = 200;
// 1.构建条件
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>().in(User::getId, ids);
// 2.自定义SQL方法调用
userMapper.updateBalanceByIds(wrapper, amount);

②在mapper方法参数中用Param注解声明wrapper变量名称,必须是ew

void updateBalanceByIds(@Param("ew") LambdaQueryWrapper<User> wrapper, @Param("amount") int amount);

③自定义SQL,并使用Wrapper条件

<update id="updateBalanceByIds">
UPDATE tb_user SET balance = balance - #{amount} ${ew.customSqlSegment}
</update>

代码示例

@Test
    void testCustomSqlUpdate() {
        //1.更新条件
        List<Long> ids = List.of(1L, 3L, 5L);
        int amount = 200;
        //2.定义条件
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>()
                .in(User::getId, ids);
        //3.调用自定义方法
        userMapper.updateBalanceByIds(wrapper,amount);
    }
//可以直接定义参数名为“ew”
//    void updateBalanceByIds(@Param("ew")LambdaQueryWrapper<User> wrapper,@Param("amount") int amount);
//也可以使用Constants.WRAPPER
    void updateBalanceByIds(@Param(Constants.WRAPPER)LambdaQueryWrapper<User> wrapper, @Param("amount") int amount);
<!--    where条件由ew.customSqlSegment表达式生成-->
<update id="updateBalanceByIds">
        Update tb_user set balance = balance + #{amount} ${ew.customSqlSegment}
</update>

MP的Service接口使用流程

自定义Service接口继承IService接口

public interface IUserService extends IService<User> {}

自定义Service实现类,实现自定义接口并继承ServiceImpl类

public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {}

3.Service接口

定义IUserService,继承IService

package com.itheima.mp.service;
​
import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.mp.domain.po.User;
​
/**
 * @Description
 * @Author LiTong(Prode)
 * @Data 2025/01/16 22:40
 */public interface IUserService extends IService<User> {
}

UserServiceImpl类,需要继承ServiceImpl,实现UserService

image.png

package com.itheima.mp.service.impl;
​
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.mapper.UserMapper;
import com.itheima.mp.service.IUserService;
import org.springframework.stereotype.Service;
​
/**
 * @Description
 * @Author LiTong(Prode)
 * @Data 2025/01/16 22:41
 */
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
}
​

最后进行测试:

package com.itheima.mp.service;
​
import com.itheima.mp.domain.po.User;
import com.itheima.mp.service.impl.UserServiceImpl;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
​
import java.time.LocalDateTime;
​
@SpringBootTest
class IUserServiceTest {
​
    @Autowired
    private UserServiceImpl userService;
​
    @Test
    void testInsert() {
        User user = new User();
        user.setId(7L);
        user.setUsername("李四");
        user.setPassword("123");
        user.setPhone("77777777");
        user.setBalance(200000);
        user.setInfo("{"age": 24, "intro": "辅导员", "gender": "female"}");
        user.setCreateTime(LocalDateTime.now());
        user.setUpdateTime(LocalDateTime.now());
        userService.save(user);
    }
​
}

4.案例:基于Restful风格实现接口

image.png

pom依赖:

        <!--swagger-->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
            <version>4.1.0</version>
        </dependency>
        <!--web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

配置swagger信息:

knife4j:
  enable: true
  openapi:
    title: 用户管理接口文档
    description: "用户管理接口文档"
    email: 1002411753@qq.com
    concat: Prode
    url: https://www.itcast.cn
    version: v1.0.0
    group:
      default:
        group-name: default
        api-rule: package
        api-rule-resources:
          - com.itheima.mp.controller

接口需要两个实体:

  • UserFormDTO:代表新增时的用户表单
  • UserVO:代表查询的返回结果
package com.itheima.mp.domain.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
 * @Description
 * @Author LiTong(Prode)
 * @Data 2025/01/17 10:14
 */
@Data
@ApiModel(description = "用户表单实体")
public class UserFormDTO {
​
    @ApiModelProperty("id")
    private Long id;
​
    @ApiModelProperty("用户名")
    private String username;
​
    @ApiModelProperty("密码")
    private String password;
​
    @ApiModelProperty("注册手机号")
    private String phone;
​
    @ApiModelProperty("详细信息,JSON风格")
    private String info;
​
    @ApiModelProperty("账户余额")
    private Integer balance;
}
package com.itheima.mp.domain.vo;
​
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
​
/**
 * @Description
 * @Author LiTong(Prode)
 * @Data 2025/01/17 10:16
 */
@Data
@ApiModel(description = "用户VO实体")
public class UserVO {
​
    @ApiModelProperty("用户id")
    private Long id;
​
    @ApiModelProperty("用户名")
    private String username;
​
    @ApiModelProperty("详细信息")
    private String info;
​
    @ApiModelProperty("使用状态(1正常 2冻结)")
    private Integer status;
​
    @ApiModelProperty("账户余额")
    private Integer balance;
}
​

Controller:

package com.itheima.mp.controller;
​
import cn.hutool.core.bean.BeanUtil;
import com.itheima.mp.domain.dto.UserFormDTO;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.domain.vo.UserVO;
import com.itheima.mp.service.IUserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
​
import java.util.List;
​
/**
 * @Description
 * @Author LiTong(Prode)
 * @Data 2025/01/17 10:17
 */
@Api(tags = "用户管理接口")
@RequiredArgsConstructor
@RestController
@RequestMapping("users")
public class UserController {
​
    //final和@RequiredArgsConstructor注解搭配使用
    private final IUserService userService;
​
    @PostMapping
    @ApiOperation("新增用户")
    public void saveUser(@RequestBody UserFormDTO userFormDTO){
        // 1.转换DTO为PO
        User user = BeanUtil.copyProperties(userFormDTO, User.class);
        // 2.新增
        userService.save(user);
    }
​
    @DeleteMapping("/{id}")
    @ApiOperation("删除用户")
    public void removeUserById(@ApiParam("用户id") @PathVariable("id") Long userId){
        userService.removeById(userId);
    }
​
    @GetMapping("/{id}")
    @ApiOperation("根据id查询用户")
    public UserVO queryUserById(@ApiParam("用户id") @PathVariable("id") Long userId){
        // 1.查询用户
        User user = userService.getById(userId);
        // 2.处理vo
        return BeanUtil.copyProperties(user, UserVO.class);
    }
​
    @GetMapping
    @ApiOperation("根据id集合查询用户")
    public List<UserVO> queryUserByIds(@ApiParam("用户id的List集合") @RequestParam("ids") List<Long> ids){
        // 1.查询用户
        List<User> users = userService.listByIds(ids);
        // 2.处理vo
        return BeanUtil.copyToList(users, UserVO.class);
    }
​
    @PutMapping("{id}/deduction/{money}")
    @ApiOperation("扣减用户余额")
    public void deductBalance(@PathVariable("id") Long id, @PathVariable("money")Integer money){
        userService.deductBalance(id, money);
    }
}
​

UserService:

package com.itheima.mp.service;
​
import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.mp.domain.po.User;
​
/**
 * @Description
 * @Author LiTong(Prode)
 * @Data 2025/01/16 22:40
 */public interface IUserService extends IService<User> {
    void deductBalance(Long id, Integer money);
}
​

UserServiceImpl:

package com.itheima.mp.service.impl;
​
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.mapper.UserMapper;
import com.itheima.mp.service.IUserService;
import org.springframework.stereotype.Service;
​
/**
 * @Description
 * @Author LiTong(Prode)
 * @Data 2025/01/16 22:41
 */
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
    @Override
    public void deductBalance(Long id, Integer money) {
        // 1.根据id查询用户
        User user = getById(id);
        // 2.判断用户状态
        if (user == null || user.getStatus() == 2) {
            throw new RuntimeException("用户状态异常");
        }
        // 3.判断用户余额
        if (user.getBalance() < money) {
            throw new RuntimeException("用户余额不足");
        }
        // 4.扣减余额
        baseMapper.deductMoneyById(id, money);
    }
}
​

mapper:

    @Update("UPDATE user SET balance = balance - #{money} WHERE id = #{id}")
    void deductMoneyById(@Param("id") Long id, @Param("money") Integer money);

5.Iservice基于Lambda的查询

image.png

定义查询条件实体

package com.itheima.mp.domain.query;
​
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
​
/**
 * @Description
 * @Author LiTong(Prode)
 * @Data 2025/02/25 21:21
 */
@Data
@ApiModel(description = "用户查询条件实体")
public class UserQuery {
    @ApiModelProperty("用户名关键字")
    private String name;
    @ApiModelProperty("用户状态:1-正常,2-冻结")
    private Integer status;
    @ApiModelProperty("余额最小值")
    private Integer minBalance;
    @ApiModelProperty("余额最大值")
    private Integer maxBalance;
}

Controller:

 @ApiOperation("根据复杂条件查询用户接口")
    @GetMapping("/list")
    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.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();
        // 3.处理vo
        return BeanUtil.copyToList(users, UserVO.class);
    }

可以发现lambdaQuery方法中除了可以构建条件,还需要在链式编程的最后添加一个list(),这是在告诉MP我们的调用结果需要是一个list集合。这里不仅可以用list(),可选的方法有:

  • .one():最多1个结果
  • .list():返回集合结果
  • .count():返回计数结果

MybatisPlus会根据链式编程的最后一个方法来判断最终的返回结果。

6.Iservice基于Lambda的更新

image.png

@Override
    @Transactional
    public void deductBalance(Long id, Integer money) {
        // 1.根据id查询用户
        User user = getById(id);
        // 2.判断用户状态
        if (user == null || user.getStatus() == 2) {
            throw new RuntimeException("用户状态异常");
        }
        // 3.判断用户余额
        if (user.getBalance() < money) {
            throw new RuntimeException("用户余额不足");
        }
//        // 4.扣减余额
//        baseMapper.deductMoneyById(id, money);
​
        // 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();
    }

7.批量新增

首先在application.yaml文件中修改MySQL配置,加入rewriteBatchedStatements=true

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: 123456

使用MyBatis-Plus的批处理

@Test
void testSaveBatch() {
    // 准备10万条数据
    List<User> list = new ArrayList<>(1000);
    long b = System.currentTimeMillis();
    for (int i = 1; i <= 100000; i++) {
        list.add(buildUser(i));
        // 每1000条批量插入一次
        if (i % 1000 == 0) {
            userService.saveBatch(list);
            list.clear();
        }
    }
    long e = System.currentTimeMillis();
    System.out.println("耗时:" + (e - b));
}

六、扩展功能

1.代码生成

下载MyBatisPlus插件 image.png 点击other->Config Database,然后输入对应的数据库信息 image.png image.png

点击other->Code Generator 进行代码生成 image.png

最终结果,生成的一些代码没有导入jar包,还需要手动进行处理 image.png

2.静态工具

有时不同service直接会互相调用,进而产生循环依赖。比如用户service当中可能注入地址service,而地址service当中也需要注入用户service。这时候就可以直接使用Db来进行。

以下是几个案例: image.png

案例1:

AddressVO:

package com.itheima.mp.domain.vo;
​
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
​
/**
 * @Description
 * @Author LiTong(Prode)
 * @Data 2025/02/26 22:53
 */
@Data
@ApiModel(description = "收货地址VO")
public class AddressVO{
​
    @ApiModelProperty("id")
    private Long id;
​
    @ApiModelProperty("用户ID")
    private Long userId;
​
    @ApiModelProperty("省")
    private String province;
​
    @ApiModelProperty("市")
    private String city;
​
    @ApiModelProperty("县/区")
    private String town;
​
    @ApiModelProperty("手机")
    private String mobile;
​
    @ApiModelProperty("详细地址")
    private String street;
​
    @ApiModelProperty("联系人")
    private String contact;
​
    @ApiModelProperty("是否是默认 1默认 0否")
    private Boolean isDefault;
​
    @ApiModelProperty("备注")
    private String notes;
}
​

在UserVO当中加入收获地址列表:

package com.itheima.mp.domain.vo;
​
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
​
import java.util.List;
​
/**
 * @Description
 * @Author LiTong(Prode)
 * @Data 2025/01/17 10:16
 */
@Data
@ApiModel(description = "用户VO实体")
public class UserVO {
​
    @ApiModelProperty("用户id")
    private Long id;
​
    @ApiModelProperty("用户名")
    private String username;
​
    @ApiModelProperty("详细信息")
    private String info;
​
    @ApiModelProperty("使用状态(1正常 2冻结)")
    private Integer status;
​
    @ApiModelProperty("账户余额")
    private Integer balance;
​
    @ApiModelProperty("收获地址列表")
    private List<AddressVO> address;
}
​

修改UserController中根据id查询用户的业务接口:

 @GetMapping("/{id}")
    @ApiOperation("根据id查询用户")
    public UserVO queryUserById(@ApiParam("用户id") @PathVariable("id") Long userId){
//        // 1.查询用户
//        User user = userService.getById(userId);
//        // 2.处理vo
//        return BeanUtil.copyProperties(user, UserVO.class);
        return userService.queryUserAndAddressById(userId);
    }

在IUserService中定义抽象方法:

package com.itheima.mp.service;
​
import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.domain.vo.UserVO;
​
/**
 * @Description
 * @Author LiTong(Prode)
 * @Data 2025/01/16 22:40
 */public interface IUserService extends IService<User> {
    void deductBalance(Long id, Integer money);
​
    UserVO queryUserAndAddressById(Long userId);
}
​

实现该方法:

 @Override
    public UserVO queryUserAndAddressById(Long userId) {
        // 1.查询用户
        User user = getById(userId);
        if (user == null) {
            return null;
        }
        // 2.查询收货地址
        //这里使用Db静态工具,而不是使用addressService,减少循环依赖风险
        List<Address> address = Db.lambdaQuery(Address.class)
                .eq(Address::getUserId, userId)
                .list();
        // 3.处理vo
        UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
        userVO.setAddress(BeanUtil.copyToList(address, AddressVO.class));
        return userVO;
    }
案例2:

这里只写service实现的部分了:

@Override
    public List<UserVO> queryUserAndAddressByIds(List<Long> ids) {
        // 1.查询用户
        List<User> users = listByIds(ids);
        if (users == null || users.isEmpty()) {
            return Collections.emptyList();
        }
        // 2查询收货地址
        //2.1获取用户id集合列表
        List<Long> userIds = users.stream().map(User::getId).collect(Collectors.toList());
        //2.2根据用户id查询地址
        List<Address> addresses = Db.lambdaQuery(Address.class)
                .in(Address::getUserId, userIds)
                .list();
        //2.3转换地址vo
        List<AddressVO> addressVOList = BeanUtil.copyToList(addresses, AddressVO.class);
        //2.4用户地址vo处理,将所有用户进行分组,然后把这些用户的地址放到相应的用户对象中
        Map<Long, List<AddressVO>> addressMap = addressVOList.stream().collect(Collectors.groupingBy(AddressVO::getUserId));
        if (CollUtil.isNotEmpty(addressVOList)) {
            addressMap = addressVOList.stream().collect(Collectors.groupingBy(AddressVO::getUserId));
        }
        //3.转换vo返回
        ArrayList<UserVO> list = new ArrayList<>(users.size());
        for (User user : users) {
            //3.1转换user的po为vo
            UserVO vo = BeanUtil.copyProperties(user, UserVO.class);
            list.add(vo);
            //3.2转换地址vo
            vo.setAddress(addressMap.get(vo.getId()));
        }
        return list;
    }

3.逻辑删除

在application.yaml中配置:

mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: deleted # 全局逻辑删除的实体字段名
      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

4.枚举处理器

在application.yaml中配置:

mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.itheima.mp.domain.po
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开启日志
    map-underscore-to-camel-case: true # 开启驼峰命名转换
    cache-enabled: false # 禁用二级缓存
    default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler # 枚举类型处理器

增加一个枚举类:

package com.itheima.mp.enums;
​
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.Getter;
​
/**
 * @Description
 * @Author lt
 * @Data 2025/02/27 12:43
 */@Getter
public enum UserStatus {
    NORMAL(1, "正常"),
    FREEZE(2, "冻结")
    ;
​
    @EnumValue
    private final int value;
    @JsonValue
    private final String desc;
​
    UserStatus(int value, String desc) {
        this.value = value;
        this.desc = desc;
    }
}
​

修改实体类以及给前端返回的实体类

package com.itheima.mp.domain.vo;
​
import com.itheima.mp.enums.UserStatus;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
​
import java.util.List;
​
/**
 * @Description
 * @Author LiTong(Prode)
 * @Data 2025/01/17 10:16
 */
@Data
@ApiModel(description = "用户VO实体")
public class UserVO {
​
    @ApiModelProperty("用户id")
    private Long id;
​
    @ApiModelProperty("用户名")
    private String username;
​
    @ApiModelProperty("详细信息")
    private String info;
​
    @ApiModelProperty("使用状态(1正常 2冻结)")
    private UserStatus status;
​
    @ApiModelProperty("账户余额")
    private Integer balance;
​
    @ApiModelProperty("收获地址列表")
    private List<AddressVO> address;
}
​
package com.itheima.mp.domain.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
 * @Description
 * @Author LiTong(Prode)
 * @Data 2025/01/17 10:14
 */
@Data
@ApiModel(description = "用户表单实体")
public class UserFormDTO {
​
    @ApiModelProperty("id")
    private Long id;
​
    @ApiModelProperty("用户名")
    private String username;
​
    @ApiModelProperty("密码")
    private String password;
​
    @ApiModelProperty("注册手机号")
    private String phone;
​
    @ApiModelProperty("详细信息,JSON风格")
    private String info;
​
    @ApiModelProperty("账户余额")
    private Integer balance;
}

5.JSON处理器

首先需要定义一个单独实体类来与User当中info字段的属性匹配:

package com.itheima.mp.domain.po;
​
import lombok.Data;
​
/**
 * @Description
 * @Author LiTong(Prode)
 * @Data 2025/02/27 17:21
 */
@Data
public class UserInfo {
​
    private Integer age;
    private String intro;
    private String gender;
    
}

接下来,将User类的info字段修改为UserInfo类型,并在注解当中声明类型处理器。同时,在User类上添加一个注解,声明自动映射。

package com.itheima.mp.domain.po;
​
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import com.itheima.mp.enums.UserStatus;
import lombok.Data;
​
import java.time.LocalDateTime;
​
@Data
@TableName(value = "tb_user",autoResultMap = true) // 表名对应数据库表 如果不写默认和数据库名一致
public class User {
​
    /**
     * 用户id
     */
    private Long id;
​
    /**
     * 用户名
     */
    private String username;
​
    /**
     * 密码
     */
    private String password;
​
    /**
     * 注册手机号
     */
    private String phone;
​
    /**
     * 详细信息
     */
    @TableField(typeHandler = JacksonTypeHandler.class)
    private UserInfo info;
​
    /**
     * 使用状态(1正常 2冻结)
     */
    private UserStatus status;
​
    /**
     * 账户余额
     */
    private Integer balance;
​
    /**
     * 创建时间
     */
    private LocalDateTime createTime;
​
    /**
     * 更新时间
     */
    private LocalDateTime updateTime;
}
​

启动报错,(提示:找不到bean对象或者xml文件的问题),需要在UserMapper.xml中指定info的类型typeHandler = com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler

<insert id="saveUser" parameterType="com.itheima.mp.domain.po.User">
        INSERT INTO `user` (`id`, `username`, `password`, `phone`, `info`, `balance`)
        VALUES
        (#{id}, #{username}, #{password}, #{phone}, #{info,typeHandler = com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler}, #{balance});
    </insert>

最后进行测试 image.png

6.插件功能

6.1分页插件

新建分页配置类

package com.itheima.mp.config;
​
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
​
/**
 * @Description
 * @Author LiTong(Prode)
 * @Data 2025/02/28 10:51
 */
@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, 3));
        // 排序参数, 通过OrderItem来指定
        p.addOrder(new OrderItem("balance", false));
        p.addOrder(new OrderItem("id", true));
​
        // 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);
    }
6.2通用分页实体

简单分页查询案例 image.png

创建一个分页查询实体,用户查询实体继承分页查询实体

package com.itheima.mp.domain.query;
​
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
​
/**
 * @Description
 * @Author LiTong(Prode)
 * @Data 2025/02/28 12:30
 */
@Data
@ApiModel(description = "分页查询实体")
public class PageQuery {
    @ApiModelProperty("页码")
    private Long pageNo;
    @ApiModelProperty("页码")
    private Long pageSize;
    @ApiModelProperty("排序字段")
    private String sortBy;
    @ApiModelProperty("是否升序")
    private Boolean isAsc;
}
​
package com.itheima.mp.domain.query;
​
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
​
/**
 * @Description
 * @Author LiTong(Prode)
 * @Data 2025/02/25 21:21
 */
@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;
}
​

封装返回实体

package com.itheima.mp.domain.dto;
​
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
​
import java.util.List;
​
/**
 * @Description
 * @Author LiTong(Prode)
 * @Data 2025/02/28 12:33
 */
@Data
@ApiModel(description = "分页结果")
@AllArgsConstructor()
public class PageDTO<T> {
    @ApiModelProperty("总条数")
    private Long total;
    @ApiModelProperty("总页数")
    private Long pages;
    @ApiModelProperty("集合")
    private List<T> list;
}
​

controller

    @ApiOperation("根据复杂条件查询用户分页接口")
    @GetMapping("/page")
    public PageDTO<UserVO> queryUsersPage(UserQuery query){
        return userService.queryUsersPage(query);
    }

Service和ServiceImpl方法

PageDTO<UserVO> queryUsersPage(UserQuery query);
    @Override
    public PageDTO<UserVO> queryUsersPage(UserQuery query) {
        String name = query.getName();
        Integer status = query.getStatus();
        // 1.构建条件
        // 1.1.分页条件
        Page<User> page = Page.of(query.getPageNo(), query.getPageSize());
        // 1.2.排序条件
        if (query.getSortBy() != null) {
            page.addOrder(new OrderItem(query.getSortBy(), query.getIsAsc()));
        }else{
            // 默认按照更新时间排序
            page.addOrder(new OrderItem("update_time", false));
        }
        // 2.分页查询
        lambdaQuery()
                .like(name != null, User::getUsername, name)
                .eq(status != null, User::getStatus, status)
                .page(page);
        // 3.数据非空校验
        List<User> records = page.getRecords();
        if (records == null || records.size() <= 0) {
            // 无数据,返回空结果
            return new PageDTO<>(page.getTotal(), page.getPages(), Collections.emptyList());
        }
        // 4.有数据,转换
        List<UserVO> list = BeanUtil.copyToList(records, UserVO.class);
        // 5.封装返回
        return new PageDTO<UserVO>(page.getTotal(), page.getPages(), list);
    }
6.3通用分页实体与MP转换(重要)

改造PageQuery实体,在PageQuery实体中定义几个工具方法,简化开发。

package com.itheima.mp.domain.query;
​
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
​
/**
 * @Description
 * @Author LiTong(Prode)
 * @Data 2025/02/28 12:30
 */
@Data
@ApiModel(description = "分页查询实体")
public class PageQuery {
    @ApiModelProperty("页码")
    private Long pageNo;
    @ApiModelProperty("页码")
    private Long pageSize;
    @ApiModelProperty("排序字段")
    private String sortBy;
    @ApiModelProperty("是否升序")
    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);
    }
}
​

改造PageDTO实体,提供查询结果转换为DTO的工具类。

package com.itheima.mp.domain.dto;
​
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
​
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
​
/**
 * @Description
 * @Author LiTong(Prode)
 * @Data 2025/02/28 12:33
 */
@Data
@ApiModel(description = "分页结果")
@AllArgsConstructor()
public class PageDTO<T> {
    @ApiModelProperty("总条数")
    private Long total;
    @ApiModelProperty("总页数")
    private Long pages;
    @ApiModelProperty("集合")
    private List<T> 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);
    }
}
​

Service层的业务代码可以简化为:

 @Override
    public PageDTO<UserVO> queryUsersPage(UserQuery query) {
        // 1.构建条件
        Page<User> page = query.toMpPageDefaultSortByCreateTimeDesc();
        // 2.查询
        page(page);
        // 3.封装返回
        return PageDTO.of(page, UserVO.class);
    }

如果是希望自定义PO到VO的转换过程,则可以在封装这里自己定义:

 @Override
    public PageDTO<UserVO> queryUsersPage(UserQuery query) {
        // 1.构建条件
        Page<User> page = query.toMpPageDefaultSortByCreateTimeDesc();
        // 2.查询
        page(page);
        // 3.封装返回
        return PageDTO.of(page, user -> {
            // 拷贝属性到VO
            UserVO vo = BeanUtil.copyProperties(user, UserVO.class);
            // 用户名脱敏
            String username = vo.getUsername();
            vo.setUsername(username.substring(0, username.length() - 2) + "**");
            return vo;
        });
    }

这里在开发过程当中可以简化很多操作,非常好用。

声明:学习视频参考B站黑马程序员(MybatisPlus-01.MybatisPlus介绍哔哩哔哩bilibili),MyBatis-Plus介绍等部分内容参考ChatGPT,详细笔记参考(‍‌‍⁠⁠‍⁠‬‍⁠‬‌‍⁠day01-MybatisPlus - 飞书云文档 (feishu.cn)