LOADING...

加载过慢请开启缓存(浏览器默认开启)

loading

MyBatisPlus

2024/11/23 后端

MyBatis-Plus
简介 | MyBatis-Plus

引入MybatisPlus起步依赖写依赖+认爸爸
  • MyBatisPlus官方提供了starter,其中集成了Mybatis和MybatisPlus的所有功能,并且实现了自动装配效果。因此我们可以用MybatisPlus的starter代替Mybatis的starter:
<!--MybatisPlus-->
<dependency>    
    <groupId>com.baomidou</groupId>    
    <artifactId>mybatis-plus-boot-starter</artifactId> 
    <version>3.5.3.1</version>
</dependency>
  • 自定义的Mapper继承MybatisPlus提供的BaseMapper接口
public interface UserMapper extends BaseMapper<User> {
}
UserMapper.java

public interface UserMapper extends BaseMapper<User> {
UserMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.mp.mapper.UserMapper">

</mapper>

常用注解

  • @TableName:用来指定表名
  • @Tableld:用来指定表中的主键字段信息
    • IdType枚举
      • AUTO:数据库自增长
      • INPUT:通过set方法自行输入
      • ASSIGN_ID分配ID 默认实现类是雪花算法
  • @TableField:用来指定表中的普通字段信息默认驼峰转下划线,不一致需要改
    • 成员变量名与数据库字段名不一致
    • 成员变量名是以is开头,且是布尔值isMarried
    • 成员变量名与数据库关键字冲突order
    • 成员变量不是数据库字段address,要标记不存在不然会默认数据库字段
@Data
public class User {
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    
    @TableField("username")
    private String name;

    @TableField("is_married")
    private Boolean isMarried;  // is经过反射会默认变成变量名Married
    
    @TableField("`order`")
    private Integer order;
    
    @TableField(exist = false)
    private String address;
......
}

此时如果数据库的表名是:tb_user 就需要用到 @TableName

数据库名:tb_user (用户表)
#  名称
1  id
2  username
3  is_married
4  order

常见配置

MyBatisPlus的配置项继承了MyBatis原生配置和一些自己特有的配置
MP更擅长单表的增删改查,如果是多表还是推荐用xml

mybatis:
  mapper-locations: classpath*:mapper/*.xml # Mapper.xml文件地址,默认值
  type-aliases-package: com.itheima.po # 别名扫描包
  configuration:
    map-underscore-to-camel-case: true # 开启驼峰命名自动映射
    cache-enabled: false # 是否开启二级缓存
  global-config:
    db-config:
      id-type: assign_id # id为雪花算法生成
      update-strategy: not_null # 更新策略:只更新非空字段 类似于动态sql

MyBatisPlus使用的基本流程

  • 引入起步依赖
  • 自定义Mapper基础BaseMapper
  • 在实体类上添加注释声明 表信息
  • 在application.yml中根据需要添加配置

核心功能—条件构造器

条件构造器

MyBatisPlus支持各种复杂的where条件,满足日常开发的所有需求

  • 查询出名字中带o的,存款大于等于1000元的人的id、username、info、balance字段
# 原始SQL:
SELECT id,username,info,balance
FROM user
WHERE username LIKE ? AND balance >= ?
// MyBatisPlus:
@Test
    void testQueryWrapper(){
        // 1.构建查询条件
        QueryWrapper<User> wrapper = new QueryWrapper<User>()
                .select("id", "username", "phone")
                .like("username", "o")
                .ge("balance", 1000);
        // 2.查询
        List<User> users = userMapper.selectList(wrapper);
        users.forEach(System.out::println);
    }
// MyBatisPlus Lambda编码格式(解决硬编码):
 @Test
    void testLambdaQueryWrapper(){
        // 1.构建查询条件
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>()
                // 利用反射 解决字符串硬编码
                .select(User::getId, User::getUsername, User::getPhone)
                .like(User::getUsername, "o")
                .ge(User::getBalance, 1000);
        // 2.查询
        List<User> users = userMapper.selectList(wrapper);
        users.forEach(System.out::println);
    }

-------------------------------------------------------------------------
@Test
void testLambdaQueryWrapperWithQueryWrapper(){
    // 1. 构建查询条件
    QueryWrapper<User> queryWrapper = new QueryWrapper<User>();
    LambdaQueryWrapper<User> wrapper = queryWrapper.lambda()
            .select(User::getId, User::getUsername, User::getPhone)
            .like(User::getUsername, "o")
            .ge(User::getBalance, 1000);

    // 2. 查询
    List<User> users = userMapper.selectList(wrapper);
    users.forEach(System.out::println);
}
  • 更新用户名为jack的用户的余额为2000
# 原始SQL:
UPDATE user
    SET balance = 2000
    WHERE (username = "jack")
// MyBatisPlus:
@Test
    void testUpdateByQueryWrapper(){
        // 1.要更新的数据
        User user = new User();
        user.setBalance(2000);
        // 2.更新的条件
        QueryWrapper<User> wrapper = new QueryWrapper<User>().eq("username", "jack");
        // 3.执行更新
        userMapper.update(user, wrapper);
    }
  • 更新id为1,2,4的用户的余额,扣200
# 原始SQL:
UPDATE user
    SET balance = balance - 200
    WHERE id in (1,2,4)
// MyBatisPlus:
@Test
    void testUpdateWrapper(){
        List<Long> ids = List.of(1L, 2L, 4L);
        UpdateWrapper<User> wrapper = new UpdateWrapper<User>()
                .setSql("balance = balance - 100")
                .in("id", ids);
        userMapper.update(null, wrapper);
    }
条件构造器的用法:
  • QueryWrapper和LambdaQueryWrapper通常用来构建select、delete、update的where条件部分

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

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

4. 字段映射与表名映射

4.1 问题一:表字段与编码属性设计不同步

  • 在模型类属性上方,使用**@TableField**属性注解,通过==value==属性,设置当前属性对应的数据库表中的字段关系。

1683796001750

4.2 问题二:编码中添加了数据库中未定义的属性

  • 在模型类属性上方,使用**@TableField注解,通过==exist==**属性,设置属性在数据库表字段中是否存在,默认为true。

1683796121907

4.3 问题三:表名与编码开发设计不同步

  • 模型类上方,使用**@TableName注解,通过==value==**属性,设置当前类对应的数据库表名称。

1683798660359

四、主键生成策略

id主键生成的策略有哪几种方式?

不同的表应用不同的id生成策略

  • 日志:自增(1,2,3,4,……)
  • 购物订单:特殊规则(FQ23948AK3843)
  • 外卖单:关联地区日期等信息(10 04 20200314 34 91)
  • 关系表:可省略id
  • ……

1 id生成策略控制(@TableId注解)

雪花算法:@TableId(type= IdType.ASSIGN_ID)
ASSIGN_UUID是趋势递增
用了分库分表就不能用默认的id自增了 要用雪花算法

  • 名称:@TableId

  • 类型:属性注解

  • 位置:模型类中用于表示主键的属性定义上方

  • 作用:设置当前类中主键属性的生成策略

  • 相关属性

    type:设置主键属性的生成策略,值参照IdType枚举值

    image-20210801192449901

2 全局策略配置

mybatis-plus:
  global-config:
    db-config:
      id-type: assign_id #全局设置主键id策略
      table-prefix: tbl_  #表名前缀设置
id生成策略全局配置

image-20210801183128266

表名前缀全局配置

image-20210801183157694

自定义SQL

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

将id在指定范围的用户(1,2,4)的余额扣减指定值
<update id = "updateBalanceByIds">
    UPDATE user
    SET balance = balance - #{amount}
    WHERE id IN
    <foreach collection="ids" separator="," item="id" open="(" close=")">
    #{id}
    </foreach>
</update>
  • 基于Wrapper构建where条件
// 1.更新条件
  List<Long> ids = List.of(1L, 2L, 4L);
  int amount = 200;
// 2.定义条件
  QueryWrapper<User> wrapper = new QueryWrapper<User>().in(User::getId, ids);
// 3.调用自定义SQL方法
  userMapper.updateBalanceByIds(wrapper, amount);
  • 在mapper方法参数中用Param注解声明wrapper变量名称,必须是ew
void updateBalanceByIds(@Param(Constants.WRAPPER) QueryWrapper<User> wrapper, @Param("amount") int amount);
  • 自定义SQL,并使用Wrapper条件
<update id="updateBalanceByIds">
        update user
        set balance = balance - #{amount} ${ew.customSqlSegment}
    </update>

IService接口基本用法

  • 自定义Service接口继承IService接口
package com.itheima.mp.service.impl;

import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.mp.domain.po.User;

public interface IUserService extends IService<User> {

}
  • 自定义Service实现类,实现自定义接口并继承ServiceImpl类
package com.itheima.mp.service.impl.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.impl.IUserService;
import org.springframework.stereotype.Service;

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

}
  • 搞了个测试类@Test
package com.itheima.mp.service.impl;

import com.itheima.mp.domain.po.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;

@SpringBootTest
class IUserServiceTest {
    @Autowired
    private IUserService userService;

    @Test
    void testSaveUser() {
        User user = new User();
        user.setId(5L);
        user.setUsername("Lucy");
        user.setPassword("123");
        user.setPhone("18688990011");
        user.setBalance(200);
        user.setInfo("{\"age\": 24, \"intro\": \"英文老师\", \"gender\": \"female\"}");
        user.setCreateTime(LocalDateTime.now());
        user.setUpdateTime(LocalDateTime.now());
        userService.save(user);
    }
    @Test
    void testQuery(){
        List<User> users = userService.listByIds(Arrays.asList(1L, 2L, 3L));
        users.forEach(System.out::println);
    }

}

IService开发基础业务接口

编号 接口 请求方式 请求路径 请求参数 返回值
1 新增用户 POST /users 用户表单实体
2 删除用户 DELETE /users/{id} 用户id
3 根据id查询用户 GET /users/{id} 用户id 用户VO
4 根据id批量查询 GET /users 用户id集合 用户VO集合
5 根据id扣减余额 PUT /users/{id}/deduction/{money} •用户id •扣减金额

解决在IDEA 的Maven下 出现 Cannot access in offline mode 问题 - Doyourself! - 博客园

管理接口文档

UserController.java
package com.itheima.mp.controller;

import cn.hutool.core.bean.BeanUtil;
import com.itheima.mp.domain.po.User;

import com.itheima.mp.domain.dto.UserFormDTO;

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;

@Api(tags = "用户管理接口")
@RequiredArgsConstructor
@RestController
@RequestMapping("users")
public class UserController {

    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(@PathVariable("id") Long userId) {
        userService.removeById(userId);
    }

    @GetMapping("/{id}")
    @ApiOperation("根据id查询用户")
    public UserVO queryUserById(@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(@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(@ApiParam("用户id") @PathVariable("id") Long id, @ApiParam("扣减的金额") @PathVariable("money") Integer money) {
        userService.deductBalance(id, money);
    }
}
UserFormDTO.java
package com.itheima.mp.domain.dto;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

@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;
}
UserQuery.java
package com.itheima.mp.domain.query;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

@Data
@ApiModel(description = "用户查询条件实体")
public class UserQuery {
    @ApiModelProperty("用户名关键字")
    private String name;
    @ApiModelProperty("用户状态:1-正常,2-冻结")
    private Integer status;
    @ApiModelProperty("余额最小值")
    private Integer minBalance;
    @ApiModelProperty("余额最大值")
    private Integer maxBalance;
}
UserServiceImpl.java
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;

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
    @Override
    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.扣减余额
        baseMapper.deductMoneyById(id, money);
    }
}
package com.itheima.mp.mapper;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.itheima.mp.domain.po.User;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;

import java.util.List;

public interface UserMapper extends BaseMapper<User> {

    List<User> queryUserByIds(@Param("ids") List<Long> ids);

    void updateBalanceByIds(@Param(Constants.WRAPPER) QueryWrapper<User> wrapper, @Param("amount") int amount);

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

Iservice的Lambda方法

需求:实现一个根据复杂条件查询用户的接口,查询条件如下:

name:用户名关键字,可以为空
status:用户状态,可以为空
minBalance:最小余额,可以为空
maxBalance:最大余额,可以为空

<select id="queryUsers" resultType="com.itheima.mp.domain.po.User">
    SELECT *
    FROM tb_user
    <where>
        <if test="name != null">
            AND username LIKE CONCAT('%', #{name}, '%')
        </if>
        <if test="status != null">
            AND `status` = #{status}
        </if>
        <if test="minBalance != null and maxBalance != null">
            AND balance BETWEEN #{minBalance} AND #{maxBalance}
        </if>
    </where>
</select>
UserQuery.java
package com.itheima.mp.domain.query;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

@Data
@ApiModel(description = "用户查询条件实体")
public class UserQuery {
    @ApiModelProperty("用户名关键字")
    private String name;
    @ApiModelProperty("用户状态:1-正常,2-冻结")
    private Integer status;
    @ApiModelProperty("余额最小值")
    private Integer minBalance;
    @ApiModelProperty("余额最大值")
    private Integer maxBalance;
}
UserController.java
    @ApiOperation("根据复杂条件查询用户接口")
    @GetMapping("/list")
    public List<UserVO> queryUsers(UserQuery query) {
        // 1.查询用户PO
        List<User> users = userService.queryUsers(query.getName(), query.getStatus(), query.getMinBalance(), query.getMaxBalance());
        // 2.把po拷贝到vo
        return BeanUtil.copyToList(users, UserVO.class);
    }
IUserService.java
package com.itheima.mp.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.mp.domain.po.User;

import java.util.List;

public interface IUserService extends IService<User> {
    void deductBalance(Long id, Integer money);

    List<User> queryUsers(String name, Integer status, Integer minBalance, Integer maxBalance);
}
UserServiceImpl.java
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;

import java.util.List;

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
   @Override
    public List<User> queryUsers(String name, Integer status, Integer minBalance, Integer maxBalance) {
        return lambdaQuery()
                .like(name!=null, User::getUsername,name)
                .eq(status!=null, User::getStatus,status)
                .gt(minBalance!=null, User::getBalance,minBalance) // 大于
                .lt(maxBalance!=null, User::getBalance,maxBalance) // 小于
                .list();
    }
}

IService的Lambda更新LambdaUpdate()、LambdaQuery()

改造根据id修改用户余额的接口,要求如下
  • 完成对用户状态校验

  • 完成对用户余额校验

  • 如果扣减后余额为0,则将用户status修改为冻结状态 (2)

    UserController.java
 @PutMapping("{id}/deduction/{money}")
    @ApiOperation("扣减用户余额")
    public void deductBalance(@ApiParam("用户id") @PathVariable("id") Long id, @ApiParam("扣减的金额") @PathVariable("money") Integer money) {
        userService.deductBalance(id, money);
    }
UserServiceImpl.java
 @Override
    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)
                .eq(User::getId, id)
                .eq(User::getBalance, user.getBalance()) // 乐观锁
                .update();
    }

IService的批量新增

批量插入10万条用户数据,并作出对比:
  • 普通for循环插入4分钟
  • IService的批量插入30秒
  • 开启rewriteBatchedStatements=true参数【6秒】重写Statement语句,在application.yaml的sql中url拼接
Test  com/itheima/mp/service/IUserServiceTest.java
@Test
    void testSaveOneByOne() {
        long b = System.currentTimeMillis();
        for (int i = 1; i <= 100000; i++) {
            userService.save(buildUser(i));
        }
        long e = System.currentTimeMillis();
        System.out.println("耗时:" + (e - b));
    }

    private User buildUser(int i) {
        User user = new User();
        user.setUsername("user_" + i);
        user.setPassword("123");
        user.setPhone("" + (18688190000L + i));
        user.setBalance(2000);
        user.setInfo("{\"age\": 24, \"intro\": \"英文老师\", \"gender\": \"female\"}");
        user.setCreateTime(LocalDateTime.now());
        user.setUpdateTime(user.getCreateTime());
        return user;
    }
MyBatisPlus的批处理
@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));
}

可以看到使用了批处理以后,比逐条新增效率提高了10倍左右,性能还是不错的。

可以发现其实MybatisPlus的批处理是基于PrepareStatement的预编译模式,然后批量提交,最终在数据库执行时还是会有多条insert语句,逐条插入数据。SQL类似这样:

Preparing: INSERT INTO user ( username, password, phone, info, balance, create_time, update_time ) VALUES ( ?, ?, ?, ?, ?, ?, ? )
Parameters: user_1, 123, 18688190001, "", 2000, 2023-07-01, 2023-07-01
Parameters: user_2, 123, 18688190002, "", 2000, 2023-07-01, 2023-07-01
Parameters: user_3, 123, 18688190003, "", 2000, 2023-07-01, 2023-07-01

而如果想要得到最佳性能,最好是将多条SQL合并为一条,像这样:

INSERT INTO user ( username, password, phone, info, balance, create_time, update_time )
VALUES 
(user_1, 123, 18688190001, "", 2000, 2023-07-01, 2023-07-01),
(user_2, 123, 18688190002, "", 2000, 2023-07-01, 2023-07-01),
(user_3, 123, 18688190003, "", 2000, 2023-07-01, 2023-07-01),
(user_4, 123, 18688190004, "", 2000, 2023-07-01, 2023-07-01);

该怎么做呢?

MySQL的客户端连接参数中有这样的一个参数:rewriteBatchedStatements。顾名思义,就是重写批处理的statement语句。参考文档:

https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-connp-props-performance-extensions.html#cj-conn-prop_rewriteBatchedStatements

这个参数的默认值是false,我们需要修改连接参数,将其配置为true

修改项目中的application.yml文件,在jdbc的url后面添加参数&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: MySQL123

扩展功能 —— 代码生成器生成代码的代码

3.1 代码生成

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

这里推荐大家使用一款MybatisPlus的插件,它可以基于图形化界面完成MybatisPlus的代码生成,非常简单。

3.1.1.安装插件

Idea的plugins市场中搜索并安装MyBatisPlus插件:

然后重启你的Idea即可使用。

3.1.2.使用

刚好数据库中还有一张address表尚未生成对应的实体和mapper等基础代码。我们利用插件生成一下。 首先需要配置数据库地址,在Idea顶部菜单中,找到other,选择Config Database

点击OK保存。

然后再次点击Idea顶部菜单中的other,然后选择Code Generator:

在弹出的表单中填写信息:

img

最终,代码自动生成到指定的位置了:

扩展功能 —— DB静态工具两个Service相互注入

  • 改造根据id查询用户的接口,查询用户的同时user表,查询出用户对应的所有地址address表
  • 改造根据id批量查询用户的接口,查询用户的同时,查询出用户对应的所有地址
  • 实现根据用户id查询收货地址功能,需要验证用户状态,冻结用户抛出异常(练习)

3.2.静态工具

有的时候Service之间也会相互调用,为了避免出现循环依赖问题,MybatisPlus提供一个静态工具类:Db,其中的一些静态方法与IService中方法签名基本一致,也可以帮助我们实现CRUD功能:

UserController.java
    @GetMapping("/{id}")
    @ApiOperation("根据id查询用户")
    public UserVO queryUserById(@PathVariable("id") Long id) {
        // 1.查询用户
//        User user = userService.getById(userId);
        // 2.处理vo
        return userService.queryUserAndAddressById(id);
    }
IUserService.java
public interface IUserService extends IService<User> {
    UserVO queryUserAndAddressById(Long id);
}
UserServiceImpl.java
@Override
    public UserVO queryUserAndAddressById(Long id) {
        // 1.查询用户
        User user = getById(id);
        if (user == null || user.getStatus() == 2) {
            throw new RuntimeException("用户状态异常");
        }
        // 2.查询方法
        List<Address> addresses = Db.lambdaQuery(Address.class)
                .eq(Address::getUserId, id).list();
        // 3.封装VO
        // 3.1 转User的PO为VO
        UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
        if (CollUtil.isEmpty(addresses)) {
           userVO.setAddresses(BeanUtil.copyToList(addresses, AddressVO.class));
        }
        return userVO;
    }

扩展功能—DB静态工具(练习)

UserController.java
@GetMapping
    @ApiOperation("根据id集合查询用户")
    public List<UserVO> queryUserByIds(@RequestParam("ids") List<Long> ids) {
        // 1.查询用户
//        List<User> users = userService.listByIds(ids);
        // 2.处理vo
        return userService.queryUserAndAddressByIds(ids);
    }
IUserService.java
public interface IUserService extends IService<User> {
    List<UserVO> queryUserAndAddressByIds(List<Long> ids);
}
UserServiceImpl.java
@Override
    public List<UserVO> queryUserAndAddressByIds(List<Long> ids) {
        // 1.查询用户
        List<User> users = listByIds(ids);
        if (CollUtil.isEmpty(users)) {
            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 梳理地址集合分组处理,分类整理,相同用户放入一个集合(组)中
        Map<Long, List<AddressVO>> addressMap = new HashMap<>(0);
        if (CollUtil.isNotEmpty(addressVOList)){
            addressMap = addressVOList.stream().collect(Collectors.groupingBy(AddressVO::getUserId));
        }
        // 3.转换VO返回
        List<UserVO> list = new ArrayList<>(users.size());
        for (User user : users) {
            // 3.1 转换User的Po为VO
            UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
            list.add(userVO);

            // 3.2 转换地址VO
            userVO.setAddresses(addressMap.get(user.getId()));
        }
        return null;
    }

扩展功能—逻辑删除要在数据库里面创建一个deleted表

订单不进行真实删除,一旦采用逻辑删除其他都不能用,需要添加配置信息

逻辑删除就是基于代码逻辑模拟删除效果,但并不会真正删除数据。思路如下:

  • 在表中添加一个字段标记数据是否被删除
  • 当删除数据时把标记置为1
  • 查询时只查询标记为0的数据

例如逻辑删除字段为deleted:

• 删除操作:

# 是0才删除 是1就不用删除 所以用AND
UPDATE user SET deleted = 1 WHERE id = 1 AND deleted = 0

• 查询操作:

# 查询未删除的数据
SELECT * FROM user WHERE deleted = 0
逻辑删除

MybatisPlus提供了逻辑删除功能,无需改变方法调用的方式,而是在底层帮我们自动修改CRUD的语句。我们要做的就是在application.yaml文件中配置逻辑删除的字段名称和值即可:

mybatis-plus: 
  global-config:    
   db-config:
    logic-delete-field: flag # 全局逻辑删除的实体字段名,字段类型可以是boolean、integer
    logic-delete-value: 1 # 逻辑已删除值(默认为 1)
    logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
逻辑删除本身也有自己的问题,比如:

• 会导致数据库表垃圾数据越来越多,影响查询效率
• SQL中全都需要对逻辑删除字段做判断,影响查询效率

因此,我不太推荐采用逻辑删除功能,如果数据不能删除,可以采用把数据迁移到其它表的办法。

扩展功能—枚举处理器

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

因此,MybatisPlus提供了一个处理枚举的类型转换器,可以帮我们把枚举类型与数据库类型自动转换

3.3.1.定义枚举

我们定义一个用户状态的枚举:

User.java //使用枚举类型
// 使用状态(1正常 2冻结)
private UserStatus status;

要让MybatisPlus处理枚举与数据库类型自动转换,我们必须告诉MybatisPlus,枚举中的哪个字段的值作为数据库值。 MybatisPlus提供了@EnumValue注解来标记枚举属性:

package com.itheima.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;
    }
}

3.3.2.配置枚举处理器MP增加了Enum和JSON处理器

在application.yaml文件中添加配置:

mybatis-plus:
  configuration:
    default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
UserVO.java
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;

@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> addresses;
}
UserServiceImpl.java
 @Override
    public UserVO queryUserAndAddressById(Long id) {
        // 1.查询用户
        User user = getById(id);
        if (user == null || user.getStatus() == UserStatus.FREEZE) {
            throw new RuntimeException("用户状态异常");
        }
        // 2.查询方法
        List<Address> addresses = Db.lambdaQuery(Address.class)
                .eq(Address::getUserId, id).list();
        // 3.封装VO
        // 3.1 转User的PO为VO
        UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
        if (CollUtil.isEmpty(addresses)) {
           userVO.setAddresses(BeanUtil.copyToList(addresses, AddressVO.class));
        }
        return userVO;
    }
想要前端返回正常还是冻结 @JsonValue
package com.itheima.mp.enums;

import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.Getter;

@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;
    }
}

扩展功能—JSON处理器AbstractJsonTypeHandler

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

名称 数据类型 注释
info JSON 详细信息
。。。。。。。。。。。。

JSON:
{ “age”:20,
“intro”: “”青年”,
“gender”:”male”}

这样一来,我们要读取info中的属性时就非常不方便。如果要方便获取,info的类型最好是一个Map或者实体类。

而一旦我们把info改为对象类型,就需要在写入数据库时手动转为String,再读取数据库时,手动转换为对象,这会非常麻烦。

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

接下来,将User类的info字段修改为UserInfo类型,并声明类型处理器:

@TableField(typeHandler = JacksonTypeHandler.class) 定义类型处理器
@TableName(value = "user", autoResultMap = true)

User.java
@Data
@TableName(value = "user", autoResultMap = true)
public class User {

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

    /**
     * 用户名
     */
    private String username;

    /**
     * 详细信息
     */
    @TableField(typeHandler = JacksonTypeHandler.class)
    private UserInfo info;
}
UserVO.java
package com.itheima.mp.domain.vo;

import com.itheima.mp.domain.po.UserInfo;
import com.itheima.mp.enums.UserStatus;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.util.List;

@Data
@ApiModel(description = "用户VO实体")
public class UserVO {

    @ApiModelProperty("用户id")
    private Long id;

    @ApiModelProperty("用户名")
    private String username;

    @ApiModelProperty("详细信息")
    private UserInfo info;

    @ApiModelProperty("使用状态(1正常 2冻结)")
    private UserStatus status;

    @ApiModelProperty("账户余额")
    private Integer balance;

    @ApiModelProperty("用户的收货地址")
    private List<AddressVO> addresses;
}
package com.itheima.mp.domain.po;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor(staticName = "of")
public class UserInfo {
    private Integer age;
    private String intro;
    private String gender;
    // 添加静态方法 of
    public static UserInfo of(Integer age, String intro, String gender) {
        return new UserInfo(age, intro, gender);
    }
}
UserMapperTest.java
@SpringBootTest
class UserMapperTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    void testInsert() {
        User user = new User();
        user.setId(5L);
        user.setUsername("Lucy");
        user.setPassword("123");
        user.setPhone("18688990011");
        user.setBalance(200);
        user.setInfo(UserInfo.of(14, "英文老师", "female"));
        user.setCreateTime(LocalDateTime.now());
        user.setUpdateTime(LocalDateTime.now());
        userMapper.insert(user);
    }

插件功能—分页插件基本用法

MyBatisPlus提供的内置拦截器有下面这些:

序号 拦截器 描述
1 TenantLineInnerInterceptor 多租户插件
2 DynamicTableNameInnerInterceptor 动态表名插件
3 PaginationInnerInterceptor 分页插件
4 OptimisticLockerInnerInterceptor 乐观锁插件
5 IllegalSQLInnerInterceptor SQL性能规范插件,检测并拦截垃圾SQL
6 BlockAttackInnerInterceptor 防止全表更新和删除的插件
首先,要在配置类中注册MyBatisPlus的核心插件,同时添加分页插件:【总拦截器】
@Configuration
public class MybatisConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        // 1. 初始化核心插件
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 2. 添加分页插件
        PaginationInnerInterceptor pageInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
        pageInterceptor.setMaxLimit(1000L); // 设置分页上限
        interceptor.addInnerInterceptor(pageInterceptor);
        return interceptor;
    }
}
接着,就可以使用分页的API了:[IService里面就有]
@Test
    void testPageQuery() {
        // 1. 查询
        int pageNo = 1, pageSize = 5;
        // 1.1. 分页参数
        Page<User> page = Page.of(pageNo, pageSize);
        // 1.2. 排序参数, 通过OrderItem来指定
        page.addOrder(new OrderItem("balance", true));
        // 先按balance排序 再按id排序
        page.addOrder(new OrderItem("id", true));
        // 1.3. 分页查询
        Page<User> p = userService.page(page);
        // 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);
    }

插件功能—通用分页实体

遵循下面的接口规范,编写一个UserController接口,实现User的分页查询
参数 说明
请求方式 GET
请求路径 /users/page
请求参数 “pageNo”: 1
“pageSize”: 5
“sortBy”: “balance”
“isAsc”: false
“name”: “jack”
“status”: 1
返回值 “total”: 1005
“pages”: 201
“list”: 包含两个元素的数组,每个元素都是一个对象,包含以下键值对:
“id”: 1 或 2
“username”: “Jack” 或 “Rose”
“info”: 包含以下键值对的对象:
“age”: 21 或 20
“gender”: “male” 或 “female”
“intro”: “佛系青年” 或 “文艺青年”
“status”: “正常” 或 “冻结”
“balance”: 2000 或 1000
特殊说明 如果排序字段为空,默认按照更新时间排序 •排序字段不为空,则按照排序字段排序
准备一下请求参数和实体【封装成xxxQuery,若只返回前端则VO,给其他使用则DTO】

写一个 【统一的分页条件】 和 【统一的分页结果】

com/itheima/mp/domain/query/UserQuery.java
// 要记得继承哦
package com.itheima.mp.domain.query;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

@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;
}
com/itheima/mp/domain/query/PageQuery.java
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;

@Data
@ApiModel(description = "分页查询实体")
public class PageQuery {
    @ApiModelProperty("页码")
    private Integer pageNo;
    @ApiModelProperty("页码个数")
    private Integer 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);
    }
}
com/itheima/mp/domain/dto/PageDTO.java
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 lombok.NoArgsConstructor;

import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;

@Data
@ApiModel(description = "分页结果")
@NoArgsConstructor
@AllArgsConstructor
public class PageDTO<V> {
    @ApiModelProperty("总条数")
    private Long total;
    @ApiModelProperty("总页数")
    private Long pages;
    @ApiModelProperty("集合")
    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);
    }
}

com/itheima/mp/controller/UserController.java
@ApiOperation("根据复杂条件查询用户接口")
    @GetMapping("/list")
    public List<UserVO> queryUsers(UserQuery query) {
        return (List<UserVO>) userService.queryUsersPage(query);
    }
// 如果你想在字符串中表示一个大于号,
你可以直接输入 >,或者使用HTML实体 &gt; 
小于号可以使用 < 或者 &lt;,
等于号可以使用 = 或者 &equals

插件功能—通用分页实体与MP转换

需求:

  • 在PageQuery中定义方法,将PageQuery对象转为MyBatisPlus中的Page对象
  • 在PageDTO中定义方法,将MyBatisPlus中的Page结果转为PageDTO结果
最好直接封装通用部分
封装查询
 @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 (StrUtil.isNotBlank(query.getSortBy())) {
            // 不为空
            page.addOrder(new OrderItem(query.getSortBy(), query.getIsAsc()));
        }else {
            // 为空,默认按照更新时间排序
            page.addOrder(new OrderItem("update_time", false));
        }
com/itheima/mp/domain/query/PageQuery.java
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;

@Data
@ApiModel(description = "分页查询实体")
public class PageQuery {
    @ApiModelProperty("页码")
    private Integer pageNo;
    @ApiModelProperty("页码个数")
    private Integer 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);
    }
}
封装结果
// 3.封装VO结果
        PageDTO<UserVO> dto = new PageDTO<>();
        // 3.1 总条数
        dto.setTotal(p.getTotal());
        // 3.2 总页数
        dto.setPages(p.getPages());
        // 3.3 当前页数据
        List<User> records = p.getRecords();
        if (CollUtil.isEmpty(records)) {
            dto.setList(Collections.emptyList());
            return dto;
        }
        // 3.4 拷贝user的VO
        dto.setList(BeanUtil.copyToList(records, UserVO.class));
        // 4.返回
        return dto;
    }
com/itheima/mp/domain/dto/PageDTO.java
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 lombok.NoArgsConstructor;

import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;

@Data
@ApiModel(description = "分页结果")
@NoArgsConstructor
@AllArgsConstructor
public class PageDTO<V> {
    @ApiModelProperty("总条数")
    private Long total;
    @ApiModelProperty("总页数")
    private Long pages;
    @ApiModelProperty("集合")
    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);
    }
}
UserServiceImpl.java
@Override
    public PageDTO<UserVO> queryUsersPage(UserQuery query) {
        String name = query.getName();
        Integer status = query.getStatus();
        // 1.构建查询条件
        // 1.1 分页条件
        Page<User> page = query.toMpPageDefaultSortByUpdateTimeDesc();
        // 2. 分页查询
        Page<User> p = lambdaQuery()
                .like(name != null, User::getUsername, name)
                .eq(status != null, User::getStatus, status)
                .page(page);
        // 3. 封装VO结果
//        return PageDTO.of(p, UserVO.class); 属性转换 ↓
        return PageDTO.of(p, user -> {
            // 1.拷贝基础属性
            UserVO vo = BeanUtil.copyProperties(user, UserVO.class);
            // 2.处理特殊逻辑 密码加**
            vo.setUsername(vo.getUsername().substring(0, vo.getUsername().length()-2)+"**");
            return vo;
        });
    }



tilas-all 成功案例

package com.itheima.domain.dto;

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class GenderStatisticsDTO {
    private String name;
    private Integer value;

}
==================================================
package com.itheima.domain.dto;

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("emp")
public class JobStatisticsDTO {
    private List<String> jobList;
    private List<Long> dataList;
}
package com.itheima.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.domain.dto.CombinedStatisticsDTO;
import com.itheima.domain.dto.GenderStatisticsDTO;
import com.itheima.domain.dto.JobStatisticsDTO;
import com.itheima.pojo.Emp;

import java.util.List;

public interface ReportService extends IService<Emp> {
    List<GenderStatisticsDTO> getGenderStatistics(); // 获取性别统计信息
    JobStatisticsDTO getJobStatistics(); // 获取职位统计信息
}


//    CombinedStatisticsDTO getCombinedStatistics();
com/itheima/controller/ReportController.java
package com.itheima.controller;

import com.itheima.domain.dto.CombinedStatisticsDTO;
import com.itheima.domain.dto.GenderStatisticsDTO;
import com.itheima.domain.dto.JobStatisticsDTO;
import com.itheima.pojo.Result;
import com.itheima.service.ReportService;
import io.swagger.annotations.Api;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@Api(tags = "报表统计接口")
@RestController
@RequestMapping("/report")
@RequiredArgsConstructor
public class ReportController {

    private final ReportService reportService;

    // 获取合并后的统计信息
//    @GetMapping("/statistics")
//    public Result<CombinedStatisticsDTO> getStatistics() {
//        CombinedStatisticsDTO combinedStatistics = reportService.getCombinedStatistics();
//        return Result.success(combinedStatistics);
//    }

    // 获取员工性别统计信息
    @GetMapping("/empGenderData")
    public Result<List<GenderStatisticsDTO>> getEmployeeGenderStatistics() {
        List<GenderStatisticsDTO> genderStatistics = reportService.getGenderStatistics();
        return Result.success(genderStatistics);
    }

    // 获取员工职位统计信息
    @GetMapping("/empJobData")
    public Result<JobStatisticsDTO> getEmployeeJobStatistics() {
        JobStatisticsDTO jobStatistics = reportService.getJobStatistics();
        return Result.success(jobStatistics);
    }
}
com/itheima/service/impl/ReportServiceImpl.java
package com.itheima.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.domain.dto.GenderStatisticsDTO;
import com.itheima.domain.dto.JobStatisticsDTO;
import com.itheima.mapper.ReportMapper;
import com.itheima.pojo.Emp;
import com.itheima.service.ReportService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Service
public class ReportServiceImpl extends ServiceImpl<ReportMapper, Emp> implements ReportService {

    private final ReportMapper reportMapper;

    @Autowired
    public ReportServiceImpl(ReportMapper reportMapper) {
        this.reportMapper = reportMapper;
    }

    // 获取员工职位统计信息
    @Override
    public JobStatisticsDTO getJobStatistics() {
        QueryWrapper<Emp> wrapper = new QueryWrapper<>();

        // 修改查询字段,注意这里你仍然需要写正确的字段名
        wrapper.select("CASE job WHEN 1 THEN '班主任' WHEN 2 THEN '讲师' WHEN 3 THEN '学工主管' WHEN 4 THEN '校研主管' WHEN 5 THEN '咨询师' ELSE '其他' END AS 职位",
                        "COUNT(*) AS 数量")
                .groupBy("job");

        // 通过 selectMaps 执行查询
        List<Map<String, Object>> statistics = reportMapper.selectMaps(wrapper);

        // 创建两个列表来存储职位和数量
        List<String> jobTitles = new ArrayList<>();
        List<Long> counts = new ArrayList<>();

        // 遍历查询结果并填充列表
        for (Map<String, Object> stat : statistics) {
            jobTitles.add((String) stat.get("职位"));
            counts.add((Long) stat.get("数量"));
        }

        // 返回JobStatisticsDTO对象,传入两个列表
        return new JobStatisticsDTO(jobTitles, counts);
    }

    // 获取员工性别统计信息
    @Override
    public List<GenderStatisticsDTO> getGenderStatistics() {
        QueryWrapper<Emp> wrapper = new QueryWrapper<>();
        wrapper.select("gender", "COUNT(gender) AS value")
                .groupBy("gender");

        List<Map<String, Object>> statistics = reportMapper.selectMaps(wrapper);

        // 转换为 GenderStatisticsDTO
        return statistics.stream()
                .map(stat -> {
                    String genderName = "1".equals(String.valueOf(stat.get("gender"))) ? "男性员工" : "女性员工";
                    int count = ((Number) stat.get("value")).intValue();
                    return new GenderStatisticsDTO(genderName, count);
                })
                .collect(Collectors.toList());
    }
}
上面的获取员工职位属性已修改为高级版本
// 获取员工职位统计信息
    @Override
    public JobStatisticsDTO getJobStatistics() {
        // 1. 构建查询条件
        QueryWrapper<Emp> wrapper = new QueryWrapper<>();
        wrapper.select("job", "COUNT(*) AS count")
                .groupBy("job");

        // 2. 查询数据
        List<Map<String, Object>> statistics = reportMapper.selectMaps(wrapper);

        // 如果返回结果为 null 或为空列表,返回默认对象
        if (statistics == null || statistics.isEmpty()) {
            return new JobStatisticsDTO(new ArrayList<>(), new ArrayList<>());
        }

        // 3. 转换结果:处理 `null` 值和字段映射
        List<String> jobTitles = new ArrayList<>();
        List<Long> counts = new ArrayList<>();

        for (Map<String, Object> stat : statistics) {
            if (stat == null) {
                continue; // 跳过 null 数据
            }

            // 使用 `getOrDefault` 方法,确保不会返回 null
            Integer jobCode = (Integer) stat.getOrDefault("job", -1);
            Long count = stat.get("count") == null ? 0L : ((Number) stat.get("count")).longValue();

            // 如果 jobCode 是 -1 或其他无效值,则视为“其他”
            String jobTitle = switch (jobCode) {
                case 1 -> "班主任";
                case 2 -> "讲师";
                case 3 -> "学工主管";
                case 4 -> "校研主管";
                case 5 -> "咨询师";
                default -> "其他";
            };

            jobTitles.add(jobTitle);
            counts.add(count);
        }

        // 返回封装好的 DTO 对象
        return new JobStatisticsDTO(jobTitles, counts);
    }