MyBatisPlus

MyBatisPlus

MyBatis-Plus 是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
特性:

  • 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
  • 损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作
  • 强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求
  • 支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错
  • 支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题
  • 支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作
  • 支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere
  • 内置代码生成器:采用代码或者 Maven 插件可快速生成 MapperModelServiceController 层代码,支持模板引擎,更有超多自定义配置等您来使用
  • 内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询
  • 分页插件支持多种数据库:支持 MySQLMariaDBOracleDB2H2HSQLSQLitePostgreSQLServer 等多种数据库
  • 内置性能分析插件:可输出 SQL 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询
  • 内置全局拦截插件:提供全表 deleteupdate 操作智能分析阻断,也可自定义拦截规则,预防误操作

作用:可以帮助我们自动生成Sql语句,实现单表的增删改查

使用MyBatisPlus 的基本步骤

  1. 引入MyBatisPlus的依赖,代替MyBatis的依赖
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
  1. 定义Mapper接口,继承BaseMapper接口

    注意

  • 继承BaseMapper接口是为了使用MyBatisPlus的通用CRUD方法
  • 继承时要指定泛型,指定要操作的数据表对应的实体类的类型 :::
public interface UserMapper extends BaseMapper<User> {
}

常见注解

MyBatisPlus通过扫描实体类,并基于反射获取实体类信息作为数据库表信息
MyBatisPlus 默认通过驼峰命名转下划线的命名规则作为数据库表字段名,如果规则不匹配必须使用注解指定表信息
MyBatisPlus 默认主键是为id字段,如果主键不是id字段必须使用注解指定主键字段 特殊情况可以通过@TableName注解指定表名,通过@TableId注解指定主键字段,通过@TableField注解指定非主键字段信息

表实体类中的常用注解

注解作用
@TableName指定表名
@TableId指定主键字段
@TableField指定非主键字段,普通字段信息

主键类型定义

@TableId注解中可以通过value属性指定主键字段名,通过type属性的IdType枚举指定主键类型,MyBatisPlus支持以下几种主键类型:

主键类型作用
AUTO数据库ID自增
INPUT通过SET方法自行输入
ASSIGN_ID分配ID,通过雪花算法生成 长度为20位

@TableName("user")
public class User {

    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    @TableField(value = "username")//不一致则需要用注解声明
    private String name;
}

使用@TableField注解的常见场景

  • @TableField 注解用于标识实体类中的字段与数据库表中的列的映射关系。如果没有显式指定字段名( value 属性),MyBatis-Plus 会按照默认的命名策略处理字段名
  • 默认命名策略即UnderlineToCamelCase,**实体类中的驼峰命名字段(如 fkCtlInst )会被转换为下划线命名(如 FK_CTL_INST ) **
  • 成员变量名与数据库字段名不一致 例如name字段,使用@TableField(value="username")标识
  • is开头且类型为布尔值的属性一定要用注解标识
  • 成员变量名与数据库关键字冲突 例如order字段,使用转义字符处理 @TableField(value="order")
  • 成员变量不是数据库字段 例如address字段,使用@TableField(exist = false)标识

常见配置

mybatis-plus:
  type-aliases-package: com.example.demo.entity #实体类地址,默认值
  mapper-locations: classpath:mapper/*.xml #Mapper.xml文件地址,默认值
  configuration:
    map-underscore-to-camel-case: true #驼峰命名转下划线
    cache-enabled: false #是否开启二级缓存,默认值
    call-setters-on-nulls: true #当查询数据为空时,是否调用映射对象的setter方法,默认值
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #日志实现,默认值
    jdbc-type-for-null: null #当查询数据为空时,jdbcType默认值
    lazy-loading-enabled: true #是否开启延迟加载,默认值
  global-config:
    db-config:
      id-type: assign_id #主键类型 雪花算法
      update-strategy: not_null #更新策略:只更新非空字段
      field-strategy: not_empty #字段策略
      table-underline: true #下划线命名
      logic-delete-value: 1 #逻辑已删除值
      logic-not-delete-value: 0 #逻辑未删除值

条件构造器

QueryWrapper条件构造器

主要是用来构建selectupdatedelete语句的where条件部分

 void text1() {
    //声明查询条件构造器 并设置查询条件 SELECT username,name,password FROM emp WHERE (job >= ? AND username LIKE ?)
    QueryWrapper<EmpPo> queryWrapper = new QueryWrapper<EmpPo>().ge("job", 3).like("username", "o");
    //查询 生成sql
    v2EmpMapper.selectList(queryWrapper).forEach(System.out::println);
}

UpdateWrapper条件构造器

主要是用来构建update语句的set条件部分

 void text3() {
    //声明更新构造器 并设置查询条件 //UPDATE emp SET name=? WHERE (username = ?)
    UpdateWrapper<EmpPo> updaterMapper = new UpdateWrapper<EmpPo>().set("name", "张三").eq("name", "赵六");

    //更新 生成sql
    v2EmpMapper.update(null, updaterMapper);
}

LambdaQueryWrapper条件构造器

主要是用来构建selectupdatedelete语句的where条件部分 可以避免硬编码

    /**
 * 查询出名字带o的 职位大于3的人的id、username、deposit
 */
void text2() {
    //声明查询条件构造器 并设置查询条件 SELECT username,name,password FROM emp WHERE (job >= ? AND username LIKE ?)
    //EmpPo::getJob 方法引用 原来 (EmpPo empPo)->empPo.getJob()
    LambdaQueryWrapper<EmpPo> lambdaQueryWrapper = new LambdaQueryWrapper<EmpPo>().ge(EmpPo::getJob, 3).like(EmpPo::getUsername, "o");

    //查询 生成sql
    v2EmpMapper.selectList(lambdaQueryWrapper).forEach(System.out::println);

}

自定义SQL

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

  1. 基于LambdaQueryWrapper构建where条件
//service
void text4() {
    //将id为 的人员的用户名+admin

    //设置where条件
    LambdaQueryWrapper<EmpPo> lambdaQueryWrapper = new LambdaQueryWrapper<EmpPo>().in(EmpPo::getId, 7, 12, 17);

    //更新 生成sql
    v2EmpMapper.setAddAdmin(lambdaQueryWrapper, "admin");
}


//mapper
@Update("UPDATE emp SET username = CONCAT(username,#{admin}) ${ew.customSqlSegment}")
void setAddAdmin(@Param(Constants.WRAPPER) Wrapper<EmpPo> wrapper, @Param("admin") String admin);

Service接口

使用前提

  • Service层接口的需要继承IService接口 并指定泛型
  • Service层的实现类需要继承ServiceImpl类 并指定mapper层接口泛型 和 实体类泛型
//service层接口
public interface EmpService extends IService<EmpPo> {
}

//service层实现类
@Service
public class EmpServiceImpl extends ServiceImpl<EmpMapper, EmpPo> implements EmpService {

    //使用
    @Override
    public void addDept(String name) {
        DeptPo deptPo = new DeptPo();
        deptPo.setName(name);
        deptPo.setCreateTime(LocalDateTime.now());
        deptPo.setUpdateTime(LocalDateTime.now());
        //使用mybatis-plus的继承ServiceImpl方法
        save(deptPo);
        //普通mapper方式
//        v2DeptMapper.insert(deptPo);
    }
}

自动填充字段

MyBatis-Plus 提供了一个便捷的自动填充功能,用于在插入或更新数据时自动填充某些字段,如创建时间、更新时间等。以下是如何使用这一功能的详细说明。

注意

  • MyBatis-Plus中,自动填充功能是根据 数据库实体类中设置的主键字段来判断是否是新增数据还是更新数据。
  • 因此,在使用自动填充功能时,请确保数据库实体类中设置了主键字段。主键字段有则代表修改,无则代表新增

1. 定义实体类

在实体类中,你需要使用 @TableField 注解来标记哪些字段需要自动填充,并指定填充的策略并指定数据库主键id

填充类型(fill)描述
INSERT新增数据时填充
UPDATE更新数据时填充
INSERT_UPDATE新增或更新数据时填充(如果更新和插入都需要填充字段则应使用此值
NONE不自动填充
public class User {
    @TableId(type = IdType.AUTO)
    private Long id;
    @TableField(fill = FieldFill.INSERT)
    private String createTime;

    @TableField(fill = FieldFill.UPDATE)
    private String updateTime;

    // 其他字段...
}

基于IServiceLambda查询

2. 实现 MetaObjectHandler

创建一个类来实现 MetaObjectHandler 接口,并重写 insertFillupdateFill 方法。


@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {

    /**
     * 插入操作,自动填充
     *
     * @param metaObject
     */
    @Override
    public void insertFill(MetaObject metaObject) {
        LocalDateTime now = LocalDateTime.now();
        Long userId = BaseContext.getCurrentId();
        log.info("开始进行插入操作的填充{}{}", now, userId);
        this.strictInsertFill(metaObject, AutoFillConstant.CREATE_TIME, LocalDateTime.class, now);
        this.strictInsertFill(metaObject, AutoFillConstant.UPDATE_TIME, LocalDateTime.class, now);
        this.strictInsertFill(metaObject, AutoFillConstant.CREATE_USER, Long.class, userId);
        this.strictInsertFill(metaObject, AutoFillConstant.UPDATE_USER, Long.class, userId);
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        LocalDateTime now = LocalDateTime.now();
        Long userId = BaseContext.getCurrentId();
        log.info("开始进行更新操作的填充{}{}", now, userId);
        this.strictUpdateFill(metaObject, AutoFillConstant.UPDATE_TIME, LocalDateTime.class, now);
        this.strictUpdateFill(metaObject, AutoFillConstant.UPDATE_USER, Long.class, userId);
    }

}

strictInsertFillstrictUpdateFill区别

strictInsertFillstrictUpdateFill 都是 MyBatis-Plus 提供的 严格模式自动填充方法,但它们的应用场景和行为有本质区别。以下是两者的对比分析

特性strictInsertFillstrictUpdateFill
触发时机插入操作时触发更新操作时触发
填充逻辑仅填充字段的初始值仅覆盖字段的更新值
典型字段create_time, create_userupdate_time, update_user
注解配合@TableField(fill = FieldFill.INSERT)@TableField(fill = FieldFill.UPDATE)

注意

  • 字段注解必须匹配:
    • 若实体类字段标注 fill = FieldFill.INSERT,则只能用 strictInsertFill
    • 若标注 fill = FieldFill.UPDATE,则只能用 strictUpdateFill
    • 只有标注fill = FieldFill.INSERT_UPDATE的字段,才能用 strictInsertFillstrictUpdateFill

IService复杂业务接口的编写

在接口定义的过程中,我们可能会遇到一些比较复杂的业务,在复杂的业务中就需要自定义Service继承继承IServiceMapper 继承BaseMapper

扣减余额示例

//service层接口
public interface UserInfoServer extends IService<UserInfoPo> {
    void UserInfoReduceById(Integer id, BigDecimal reduceMoney);
}

//实现类
@RequiredArgsConstructor
@Service
public class UserInfoServerImpi extends ServiceImpl<UserInfoMapper, UserInfoPo> implements UserInfoServer {
    @Override
    public void UserInfoReduceById(Integer userId, BigDecimal reduceMoney) {
        //1.查询用户
        UserPo myUser = userMapper.selectById(userId);
        //2.校验用户状态
        if (myUser == null)
            throw new RuntimeException("用户不存在");
        else if (myUser.getStatus() == 0)
            throw new RuntimeException("用户已被禁用");
        //3.校验余额是否充足
        LambdaQueryWrapper<UserInfoPo> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(UserInfoPo::getUserId, userId);
        UserInfoPo userInfoPo = this.getOne(lambdaQueryWrapper);
        if (userInfoPo.getBalance().subtract(reduceMoney).compareTo(new BigDecimal("0")) < 0) {
            throw new RuntimeException("余额不足");
        }
        //4.扣减余额
        userInfoMapper.updateBalanceByUserId(lambdaQueryWrapper, reduceMoney);
    }
}

//mapper接口
@Mapper
public interface UserInfoMapper extends BaseMapper<UserInfoPo> {
    //扣减余额
    //使用自定义sql拼接where语句
    @Update("update user_info set balance = balance - #{balance} ${ew.customSqlSegment}")
    void updateBalanceByUserId(@Param(Constants.WRAPPER) Wrapper<UserInfoPo> wrapper, BigDecimal balance);
}

分页

添加MybatisPlus的分页拦截器

import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        //添加分页拦截器
        // 如果有多数据源可以不配具体类型, 否则都建议配上具体的 DbType
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 如果配置多个插件, 切记分页最后添加
        //添加阻止全表更新的拦截器 自动拦截不带 where的更新操作
        interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
        return interceptor;
    }
}

注意

mybatis-plus v3.5.9 起,PaginationInnerInterceptor 已分离出来。如需使用,则需单独引入 mybatis-plus-jsqlparser 依赖

    <!--        mybatisPlus分页插件-->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-jsqlparser</artifactId>
    <version>3.5.9</version>
</dependency>

通用分页接收实体

package com.sky.entry;

import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiModelProperty;
import lombok.Builder;
import lombok.Data;

// 分页查询数据
@Api(tags = "分页查询数据")
@Data
@Builder
public class PageQueryDTO {
    //@ApiModelProperty 它被应用在数据传输对象(DTO)或视图对象(VO)的字段上,用来描述这些字段在 API 文档中的相关信息
    @ApiModelProperty("页码")
    private Integer page = 1;
    @ApiModelProperty("每页数据量")
    private Integer pageSize = 10;
    @ApiModelProperty("排序字段")
    private String sort;
    @ApiModelProperty("是否升序")
    private Boolean isAsc = true;

    public <T> Page<T> toPage(OrderItem... items) {
        Page<T> pageInfo = Page.of(page, pageSize);
        if (!StringUtils.isEmpty(sort)) {
            pageInfo.addOrder(new OrderItem(sort, isAsc));
        } else if (items != null && items.length > 0) {
            pageInfo.addOrder(items);
        }
        return pageInfo;
    }

    public <T> Page<T> toPageDefultSortByUpdateTime() {
        return toPage(new OrderItem("update_Time", false));
    }

    public <T> Page<T> toPageDefultSortByCreateTime() {
        return toPage(new OrderItem("create_Time", false));
    }

}


通用分页返回实体

package com.sky.result;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;

import java.util.List;
import java.util.function.Function;

@EqualsAndHashCode(callSuper = true)
@Data
@ApiModel("分页查询实体")
public class PageResultVo<T> extends PageResult<T> {
    @ApiModelProperty("当前页")
    private Long page;//当前页
    @ApiModelProperty("每页条数")
    private Long pageSize;//每页条数

    public static <T> PageResultVo<T> success(Page<T> page) {
        PageResultVo<T> result = new PageResultVo<>();
        //设置当前页
        result.setPage(page.getCurrent());
        //设置每页条数
        result.setPageSize(page.getSize());
        //设置总记录数
        result.setTotal(page.getTotal());
        //设置当前页数据
        result.setRecords(page.getRecords());
        //返回结果
        return result;
    }

    public static <T, K> PageResultVo<K> successConverter(Page<T> page, Function<T, K> converter) {
        PageResultVo<K> result = new PageResultVo<>();
        //设置当前页
        result.setPage(page.getCurrent());
        //设置每页条数
        result.setPageSize(page.getSize());
        //设置总记录数
        result.setTotal(page.getTotal());
        //类型转换
        List<K> list = page.getRecords().stream().map(converter).toList();
        //设置当前页数据
        result.setRecords(list);
        //返回结果
        return result;
    }
}

多表查询

多表查询,即查询多个表,并且多个表之间有关联关系。

一对一联合查询单条记录(例如一个员工属于一个部门)

  1. 在实体类中加入给自身赋值的实体类

@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("emp")
public class EmpPo implements Serializable {
    @TableId(value = "id", type = IdType.AUTO)
    @Schema(description = "主键")
    private Integer id;

    //    ----
    //为自身赋值的构造器
    //用于通过已有EmpPo对象创建新对象,实现对象属性的深拷贝
    public EmpPo(EmpPo empPo) {
        this.id = empPo.getId();
//        ----
    }
}
  1. 定义Vo类继承该实体类 并加入属性

@Data
public class UserVo extends EmpPo {
    //部门名称
    private String deptName;

    //使用构造器构造出继承的实体类
    public UserVo(EmpPo empPo) {
        super(empPo);
    }

}
  1. 编写查询结果

@GetMapping("/getEmpById")
public Result<EmpPo> getEmpById(@RequestParam Integer id) {
    //获取员工数据 Emp类型
    EmpPo emp = empServer.getById(id);
    //将员工Emp类型转换成UserVo类型
    UserVo userVo = new UserVo(emp);
    //获取部门数据
    DeptPo dept = deptServer.getById(userVo.getDeptId());
    userVo.setDeptName(dept.getName());
    return Result.success(userVo);
}

一对多查询多条记录(例如一个员工具有多个地址)

实体类同上

//vo
@Data
public class UserVo extends EmpPo {
    //部门名称
    private String deptName;

    //使用构造器构造出继承的实体类
    public UserVo(EmpPo empPo) {
        super(empPo);
    }

}

@GetMapping("/getEmpById")
public Result<EmpPo> getEmpById(@RequestParam Integer id) {
//获取员工数据 Emp类型
    EmpPo emp = empServer.getById(id);
//将员工Emp类型转换成UserVo类型
    UserVo userVo = new UserVo(emp);
//获取部门数据
    DeptPo dept = deptServer.getById(userVo.getDeptId());
    userVo.setDeptName(dept.getName());

    //获取地址数据
    List<EmpAddressPo> empAddressPoList = empAddresServer.lambdaQuery().eq(EmpAddressPo::getUserId, id).list();
    userVo.setEmpAddressPoList(empAddressPoList);
    return Result.success(userVo);

}

多表联合列表查询员工数据

实体类同上

 /**
 * 条件查询员工数据
 */
//注释注解
@Operation(summary = "条件查询用户列表", description = "包含详情")
@GetMapping("/query")
public Result<List<UserVo>> query(@Parameter(description = "员工id") @RequestParam(required = false) Integer empId,
                                  @Parameter(description = "员工姓名") @RequestParam(required = false) String empName,
                                  @Parameter(description = "员工性别") @RequestParam(required = false) Short sex,
                                  @Parameter(description = "员工职位") @RequestParam(required = false) Short job,
                                  @Parameter(description = "部门id") @RequestParam(required = false) Integer deptId) {
    List<EmpPo> select = empServer.lambdaQuery().eq(empId != null, EmpPo::getId, empId)
            .eq(empName != null, EmpPo::getUsername, empName)
            .eq(job != null, EmpPo::getJob, job)
            .eq(deptId != null, EmpPo::getDeptId, deptId)
            .eq(sex != null, EmpPo::getSex, sex)
            .list();
    //将员工List<EmpPo>转换成List<UserVo>
    List<UserVo> userVoList = select.stream().map(UserVo::new).toList();

    //查询部门数据
    //员工list所有的部门id遍历成一个集合(使用set集合去重)
    Set<Integer> deptIds = userVoList.stream().map(EmpPo::getDeptId).collect(Collectors.toSet());
    //查找集合中所有的部门数据
    List<DeptPo> deptPoList = deptServer.lambdaQuery().in(DeptPo::getId, deptIds).list();
    //将部门数据List<DeptPo>转换成Map<Integer,String> 按照部门id进行分组
    Map<Integer, String> deptMap = deptPoList.stream().collect(Collectors.toMap(DeptPo::getId, DeptPo::getName));
    //在userVoList中根据部门id插入部门名称
    userVoList.forEach(userVo -> userVo.setDeptName(deptMap.get(userVo.getDeptId())));

    //查询地址数据(同理进行分组)
    List<Integer> userIds = userVoList.stream().map(EmpPo::getId).toList();
    //查询地址数据
    List<EmpAddressPo> empAddressPoList = empAddresServer.lambdaQuery().in(EmpAddressPo::getUserId, userIds).list();
    //将地址根据人员id进行分组
    Map<Integer, List<EmpAddressPo>> empAddressPoMap = empAddressPoList.stream().collect(Collectors.groupingBy(EmpAddressPo::getUserId));
    //循环插入地址数据
    userVoList.forEach(userVo -> {
        //没有地址返回空数组
        userVo.setEmpAddressPoList(Objects.requireNonNullElseGet(empAddressPoMap.get(userVo.getId()), List::of));
    });
    return Result.success(userVoList);
}

多表链接查询一对一分页查询

    public PageResult<UserVo> empPage(Integer pageNum, Integer pageSize, Integer sex, Integer job) {
    //分页查询员工数据
    //分页查询user表中员工数据
    //使用员工表作为主要分页的载体往查询出来的员工数据中进行分页操作
    //使用mybatisplus的分页插件
    Page<EmpPo> page = new Page<>(pageNum, pageSize);
    //设置排序条件
    page.addOrder(new OrderItem().setAsc(false, EmpPo::getId));
    //构造查询条件
    Page<EmpPo> pageList = this.lambdaQuery().eq(sex != null, EmpPo::getSex, sex)
            .eq(job != null, EmpPo::getJob, job).page(page);

    //将查询出来的数据补充上其他数据

    // //将员工List<EmpPo>转换成List<UserVo>
    List<UserVo> userVoList = pageList.getRecords().stream().map(UserVo::new).toList();
    //没有数据直接返回空数组
    if (userVoList.isEmpty()) return PageResult.<UserVo>builder().build();
    //查询列表中所有的部门数据 使用set去重
    Set<Integer> deptIds = userVoList.stream().map(EmpPo::getDeptId).collect(Collectors.toSet());
    //查找集合中所有的部门数据
    List<DeptPo> deptPoList = deptServer.lambdaQuery().in(DeptPo::getId, deptIds).list();
    //将部门数据List<DeptPo>转换成Map<Integer,String> 按照部门id进行分组
    Map<Integer, String> deptMap = deptPoList.stream().collect(Collectors.toMap(DeptPo::getId, DeptPo::getName));
    //将部门数据插入到实体中
    userVoList.forEach(userVo -> userVo.setDeptName(deptMap.get(userVo.getDeptId())));

    //查询地址数据
    //查询列表中所有员工id
    List<Integer> userIds = userVoList.stream().map(EmpPo::getId).toList();
    //查询相应的员工地址数据
    List<EmpAddressPo> empAddressPoList = empAddressServer.lambdaQuery().in(EmpAddressPo::getUserId, userIds).list();
    //将地址数据根据人员id进行分组
    Map<Integer, List<EmpAddressPo>> empAddressPoMap = empAddressPoList.stream().collect(Collectors.groupingBy(EmpAddressPo::getUserId));
    //将地址数据插入到实体中
    userVoList.forEach(userVo -> userVo.setEmpAddressPoList(empAddressPoMap.get(userVo.getId())));
    //构造返回结果
    //建造者写法
    return PageResult.<UserVo>builder()
            .total(pageList.getTotal())
            .records(userVoList)
            .build();
}

多表联合查询时推荐类写法

//用于自动生成 equals() 和 hashCode() 方法
@EqualsAndHashCode(callSuper = true)
@Data
public class UserVo extends EmpPo {
    @Schema(description = "部门名称")
    private String deptName;
    @Schema(description = "员工地址")
    private List<EmpAddressPo> empAddressPoList;

    public UserVo(EmpPo empPo) {
        super(empPo);
    }

    //注入部门名称和员工地址
    public void addDeptName(DeptPo deptPo) {
        this.deptName = deptPo.getName();
    }

    //注入员工地址
    public void addEmpAddressPoList(List<EmpAddressPo> empAddressPoList) {
        this.empAddressPoList = empAddressPoList;
    }

}

多表联合查询一对多分页查询

//DeptPo
package com.springboot.pojo.po;

@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("dept")
public class DeptPo {
    @TableId(value = "id", type = IdType.AUTO)
    @Schema(description = "部门id")
    private Integer id;
    @Schema(description = "部门名称")
    private String name;
    @Schema(description = "创建时间")
    private LocalDateTime createTime;
    @Schema(description = "更新时间")
    private LocalDateTime updateTime;

    public DeptPo(DeptPo deptPo) {
        //使用BeanUtils.copyProperties()方法快速赋值
        BeanUtils.copyProperties(deptPo, this);
    }
}


// DeptVo
package com.springboot.pojo.vo;

//部门表包含人员信息数据
@EqualsAndHashCode(callSuper = true)
@Data
public class DeptVo extends DeptPo {
    List<EmpPo> EmpPoList;

    public DeptVo(DeptPo deptPo) {
        //将外界传递过来的部门信息复制给当前对象
        super(deptPo);
    }
}


@Slf4j
@Service
@RequiredArgsConstructor
public class DeptServerImp extends ServiceImpl<DeptMapper, DeptPo> implements DeptServer {
    private EmpServer empServer;

    @Autowired
    public DeptServerImp(@Lazy EmpServer empServer) {
        //直接注入具有循环依赖问题
        //使用 Setter/Field 注入代替构造器注入 使用@Lazy注解 延迟加载容器中的bean类
        this.empServer = empServer;
    }

    @Override
    public PageResult<DeptVo> getDeptList(Integer pageSize, Integer pageNum) {
        Page<DeptPo> page = new Page<>(pageNum, pageSize);
        //根据分页获取部门数据
        Page<DeptPo> deptVoPage = this.lambdaQuery().page(page);
        //获取分页结果
        List<DeptPo> records = deptVoPage.getRecords();

        //查询人员数据
        //获取所有的部门id
        List<Integer> ids = records.stream().map(DeptPo::getId).toList();
        //根据部门id查询所有的人员
        List<EmpPo> empPoList = empServer.lambdaQuery().in(EmpPo::getDeptId, ids).list();
        //将人员数据List<EmpPo>转换成Map<Integer,List<EmpPo>> 按照部门id进行分组
        Map<Integer, List<EmpPo>> empMap = empPoList.stream().collect(Collectors.groupingBy(EmpPo::getDeptId));
        //利用循环将列表中的每一项转换成DeptVo 并将人员数据赋给DeptVo
        List<DeptVo> list = records.stream().map(item -> {
            //转换成DeptVo
            DeptVo deptVo = new DeptVo(item);
            //赋值人员列表
            deptVo.setEmpPoList(empMap.get(item.getId()));
            return deptVo;
        }).toList();
        //返回数据
        return new PageResult<DeptVo>(deptVoPage.getTotal(), list);
    }
}

批量删除


@Service
@RequiredArgsConstructor
public class DishServiceImpi extends ServiceImpl<DishMapper, Dish> implements DishService {
    private final DishFlavorService dishFlavorService;
    private final DishMapper dishMapper;
    private final CategoryService categoryService;
    private final SetmealDishService setmealDishService;

    //加入事物标签 删除失败时进行回滚
    @Transactional
    @Override
    public void deleteDish(List<Long> ids) {
        //将字符串以逗号分隔成数
        if (ids.isEmpty()) throw new RuntimeException("请选择要删除的菜品");
        //查找套餐表查询是否有关联的套餐
        if (setmealDishService.lambdaQuery().in(SetmealDish::getDishId, ids).count() > 0)
            throw new RuntimeException("当前菜品有关联的套餐,不能删除");
        //查询当前菜品是否在启用状态
        List<Dish> list = this.lambdaQuery().in(Dish::getId, ids).list();
        //anyMatch 查询是否有一个元素满足条件 如果找到第一个元素则立即返回true
//        list.stream().anyMatch(item -> item.getStatus() == 1)


//        ifPresent 是 Optional 类的一个重要方法,用于在 Optional 包含值时执行指定的操作。
//        如果 Optional 包含非空值,则执行给定的 Consumer 操作
//        如果 Optional 为空,则不执行任何操作
//        这是一个终端操作,不返回任何值(void)
        list.stream().filter(item -> item.getStatus() == 1).findFirst().ifPresent(item -> {
            String pattern = "当前菜品【{0}】正在启售中,不能删除";
            throw new DeletionNotAllowedException(MessageFormat.format(pattern, item.getName()));
        });
        //删除口味表数据
        this.removeByIds(dishFlavorService.lambdaQuery().in(DishFlavor::getDishId, ids).list());
        //删除菜品表数据
        this.removeByIds(ids);
    }
}


静态工具解决循环依赖问题

循环依赖问题是 Spring 框架中常见的一个问题,特别是在使用 MyBatis-Plus 进行多表关联查询时更容易出现。

什么是循环依赖?

循环依赖是指两个或多个 bean 互相持有对方的引用,形成一个闭环。例如:

  • A 依赖 B,B 依赖 A(直接循环依赖)
  • A 依赖 B,B 依赖 C,C 依赖 A(间接循环依赖)

解决方案

方案一 使用 @Lazy 注解

  • 在注入依赖的地方加上 @Lazy 注解,延迟加载 bean 实例

@Autowired
public DeptServerImp(@Lazy EmpServer empServer) {
    this.empServer = empServer;
}

方案二 mybatis-plus 提供的静态工具DB直接不注入bean直接使用


@Service
public class DeptServerImp implements DeptServer {
    PageResult<DeptVo> getDeptList(Integer pageSize, Integer pageNum) {
        //使用DB静态工具 传入数据库表实体类 从而实现不注入 bean 进而操作数据库表
        List<EmpPo> empPoList = Db.lambdaQuery(EmpPo.class).in(EmpPo::getDeptId, ids).list();
        return null;
    }
}

逻辑删除

  • 逻辑删除是 MyBatis-Plus 提供的 API,用于在数据库中将数据标记为删除,而不是真正的删除数据。
  • 在查询时无需改变方法的调用方式,底层自动添加了 WHERE 条件,查询结果中将不包含被逻辑删除的数据。

配置

appication.yml中添加如下配置:

mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: is_deleted # 全局逻辑删除的实体字段名 字段类型可以是boolean 、integer
      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

注意

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

  • 会导致数据库中的垃圾数据越来越多,影响查询效率
  • sql中全部需要对逻辑删除字段做判断,影响查询效率 因此不太推荐逻辑删除功能,如果数据不能删除,可以采用把数据迁移到其他表的方法

枚举处理器

枚举处理器(Enum Handler)MyBatis-Plus 提供的一种机制,用于处理 Java 枚举类型与数据库字段之间的映射关系。
枚举处理器解决了什么问题:

  1. 枚举类型与数据库字段的自动映射

    • 在实际开发中,经常使用枚举类型来表示状态、类型等固定选项,如性别(男/女)、订单状态(待支付/已支付/已完成)等。
    • 默认情况下,MyBatis 无法直接将 Java 枚举类型映射到数据库字段,需要手动处理。
    • 枚举处理器允许你自定义枚举值如何存储到数据库(如存枚举的名称或序号),并在从数据库读取时正确还原为 Java 枚举对象。
  2. 提升代码可读性和维护性

    • 使用枚举代替硬编码的数字或字符串,使代码更易理解。
    • 结合枚举处理器后,可以直接在实体类中使用枚举类型,而无需额外的转换逻辑。
  3. 统一管理枚举值

    • 可以为不同的枚举类型定义各自的处理器,集中管理和控制不同业务含义的枚举值如何持久化。
public enum UserStatus {
    ACTIVE(1, "激活"),
    INACTIVE(0, "未激活");
    // @EnumValue 标识映射的数据库字段
    @EnumValue
    private final int code;
    private final String desc;

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

    public int getCode() {
        return code;
    }
}

通过配置枚举处理器,可以实现在数据库中保存 code 值(例如 10),而在 Java 应用中使用 UserStatus.ACTIVE 这样的枚举常量。

配置

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

JSON处理器

在数据库表中有一个json类型的字段


//开启自动映射
@TableName(value = "user", autoResultMap = true)
public class User {
    //标识json字段
    @TableField(typeHandler = JsonTypeHandler.class)
    private UserInfo userInfo;
}
上次更新 2025/11/25 20:07:44