黑马商城day1-MyBatis-Plus

一.快速入门

1.1入门案例

1.引入MyBatis-Plus依赖代替MyBatis依赖

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

2.让原来的Mapper接口 继承BaseMapper<泛型>

1.2 常见注解

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

生成sql的约定:

  1. 类名驼峰转下划线作为表名
  2. 名为id的字段作为主键
  3. 变量名驼峰转下划线作为表的字段名

常见注解:

  1. @TableName:用来指定表名
  2. @TableId:用来指定表中的主键字段信息
  3. @TableField:用来指定表中的普通字段信息

IdType枚举:

  • AUTO:数据库自增长
  • INPUT:通过set方法自行输入
  • ASSIGN_ID:分配 ID,接口IdentifierGenerator的方法nextId来生成id,默认实现类为DefaultIdentifierGenerator雪花算法。

使用@TableField的常见场景:

  • 成员变量名与数据库字段名不一致
  • 成员变量名以is开头,且是布尔值。Java 反射在处理布尔类型的 getter 方法时,会自动忽略字段名中的 is 前缀。这种情况下,MyBatis-Plus 会默认按照反射解析的逻辑名称 Married 去匹配数据库字段,但若数据库字段实际是 is_married (而非 Married ),就会出现映射失败。
  • 成员变量名与数据库关键字冲突(`order`)
  • 成员变量不是数据库字段

1.3常见配置

1.4 总结

MyBatisPlus使用基本流程:

  1. 引入起步依赖
  2. 自定义Mapper继承BaseMapper<泛型>
  3. 在实体类上添加注解声明 数据库表信息
  4. 在application.yml中根据需要添加配置

二. 核心功能

2.1 条件构造器

MyBatis-Plus 的条件构造器(Wrapper)是用于动态构建 SQL 查询条件的工具,它可以让你无需手动编写复杂的 SQL 语句,通过 Java 代码的方式灵活拼接查询条件,大幅简化数据查询操作。

简单来说,条件构造器就是用面向对象的方式来替代传统的 SQL 条件拼接,尤其适合需要根据不同业务场景动态生成查询条件的场景(比如多条件筛选、复杂查询等)。

核心作用:

  1. 动态构建 WHERE 条件 :根据业务逻辑动态添加AND/OR=, >, <, LIKE, IN等条件
  2. 避免 SQL 注入风险:自动处理参数绑定,比直接拼接 SQL 字符串更安全
  3. 简化代码 :无需编写冗长的 XML 映射文件或@Select注解 SQL

常用的条件构造器:

  • QueryWrapper:最常用,用于构建查询条件
  • UpdateWrapper:用于构建更新条件
  • LambdaQueryWrapper:基于 Lambda 表达式,避免硬编码字段名(推荐)

QueryWrapper查询

java 复制代码
    @Test
    void testQueryWrapper(){
        //查询出名字中带o的,存款大于等于1000元的人的id、username、info、balance字段
        //select id,username,info,balance from user where username like ? and balance >= ?
        //1.构建查询条件
        QueryWrapper<User> wrapper = new QueryWrapper<User>()
                .select("id","username","info","balance")
                .like("username","o")
                .ge("balance",1000);//ge是大于等于,gt是大于
        //2.查询
        List<User> users = userMapper.selectList(wrapper);
        users.forEach(System.out::println);
    }

QueryWrapper更新:

java 复制代码
    @Test
    void testUpdateByQueryWrapper(){
        //更新用户名为jack的用户的余额为2000
        //update user set balance=2000 where username=jack
        //1.要更新的数据
        User user = new User();
        user.setBalance(2000);
        //2.更新条件
        QueryWrapper<User> wrapper = new QueryWrapper<User>()
                .eq("username","jack");
        //3.更新
        userMapper.update(user,wrapper);
    }

UpdateWrapper更新:

java 复制代码
    @Test
    void testUpdateWrapper(){
        //更新id为1,2,4的用户的余额,扣200
        //update user set balance=balance-200 where id in (?,?,?)
        //1.更新条件
        List<Long> ids = List.of(1L,2L,4L);

        UpdateWrapper<User> updateWrapper = new UpdateWrapper<User>()
                .setSql("balance = balance -200")
                .in("id",ids);
        //2.更新
        userMapper.update(null,updateWrapper);
    }

LambdaQueryWrapper查询:

java 复制代码
    @Test
    void testLambdaQueryWrapper(){
        //查询出名字中带o的,存款大于等于1000元的人的id、username、info、balance字段
        //select id,username,info,balance from user where username like ? and balance >= ?
        //1.构建查询条件
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>()
                .select(User::getId,User::getUsername,User::getInfo,User::getBalance)
                .like(User::getUsername,"o")
                .ge(User::getBalance,1000);//ge是大于等于,gt是大于
        //2.查询
        List<User> users = userMapper.selectList(wrapper);
        users.forEach(System.out::println);
    }

条件构造器的用法:

  • QueryWrapper和LambdaQueryWrapper通常用来构建select、delete、update的where条件部分
  • UpdateWrapper和LambdaUpdateWrapper通常只有在set语句比较特殊才使用
  • 尽量使用LambdaQueryWrapper和LambdaUpdateWrapper,避免硬编码

2.2 自定义SQL

直接使用Wrapper存在问题:在Service层编写sql语句,这不符合开发规范。

以上两个案例都存在,如果完全使用Wrapper不可避免在Service层编写sql语句。为了解决这一问题,我们可以通过 自定义sql语句前半部分,后半部分where条件通过Wrapper定义,来实现。

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

1.基于Wrapper构建where条件:

java 复制代码
    @Test
    void testUpdate2ByQueryWrapper(){
        //更新用户id在范围内的用户balance减去固定值
        //1.基于Wrapper构建where条件
        List<Long> ids = List.of(1L,2L,4L);
        int amount = 200;
        QueryWrapper<User> wrapper = new QueryWrapper<User>()
                .in("id",ids);
        //2.将条件传入mapper层
        userMapper.updateBalanceByIds(wrapper,amount);
    }

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

java 复制代码
 void updateBalanceByIds(@Param(Constants.WRAPPER) QueryWrapper<User> wrapper,@Param("amount") int amount);

3.自定义SQL,并使用Wrapper条

java 复制代码
    <update id="updateBalanceByIds">
        update tb_user set balance = balance - #{amount} ${ew.customSqlSegment}
    </update>

2.3 IService接口

核心特点:

  • 提供了大量现成的方法(getByIdlistsaveupdate 等),无需手动编写 SQL
  • 支持批量操作(saveBatchupdateBatchById 等),简化批量处理逻辑
  • 可以结合条件构造器(Wrapper)实现复杂查询
  • 遵循 Service 层规范,便于业务逻辑扩展

简单来说,IService 让我们不用重复编写基础的 CRUD 代码,专注于业务逻辑的实现,极大提高了开发效率。

2.3.1 IService接口基本用法

实现方法:UserService继承IService,UserServiceImpl继承ServiceImpl。接口继承接口,实现类继承实现类。

MP的Service接口使用流程:

  • 自定义Service接口继承IService接口

    java 复制代码
    public interface IUserService extends IService<User> {}

    此时就可以直接注入IUserService,调用IService中的方法。

    java 复制代码
    @SpringBootTest
    class IUserServiceTest {
        @Autowired
        private IUserService userService;
        @Test
        public void testList(){
            List<User> list = userService.list();
            list.forEach(System.out::println);
        }
    }

2.3.2 基本接口开发

课程内容:根据需求以及接口定义,利用IService实现简单功能不用编写Service层程序,实现接口功能。

java 复制代码
@RestController
@RequestMapping("/users")
@Api(tags = "用户管理接口")
@Slf4j
@RequiredArgsConstructor
public class UserController {

    private final IUserService userService;

    @PostMapping
    @ApiOperation(value = "新增用户接口")
    public void saveUser(@RequestBody UserFormDTO userFormDTO){
        User user = BeanUtil.copyProperties(userFormDTO,User.class);
        userService.save(user);
    }

    @DeleteMapping("{id}")
    @ApiOperation(value = "删除用户接口")
    private void deleteUserById(@ApiParam("用户ID") @PathVariable Long id){
        userService.removeById(id);
    }

    @GetMapping("{id}")
    @ApiOperation(value = "根据id查询用户接口")
    private UserVO queryUserById(@ApiParam("用户ID") @PathVariable Long id){
        //1.查询用户PO
        User user = userService.getById(id);
        //2.把PO拷贝到VO
        return BeanUtil.copyProperties(user,UserVO.class);
    }

    @GetMapping
    @ApiOperation(value = "根据id批量查询用户接口")
    private List<UserVO> queryBatchUserById(@ApiParam("用户ID集合") @RequestParam("ids") List<Long> ids){
        //1.查询用户PO
        List<User> users= userService.listByIds(ids);
        //2.把PO拷贝到VO
        return BeanUtil.copyToList(users,UserVO.class);
    }
}
java 复制代码
@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);
    }
}
java 复制代码
    @Update("update tb_user set balance = balance - #{money} where id = #{id}")
    void deductMoneyById(@Param("id") Long id,@Param("money") Integer money);
新知识:构造器注入;BeanUtil;反向判断。

1.关于BeanUtil:

User user = BeanUtil.copyProperties(userFormDTO,User.class);

List<UserVO> list = BeanUtil.copyToList(users,UserVO.class);

2.关于@Autowired注解,编译器默认不推荐,而推荐使用构造器注入。

直接使用 @Autowired 进行字段注入存在一些潜在问题,这些问题在复杂项目中可能导致代码可读性差、可测试性低或隐藏设计缺陷。下面通过具体例子说明为什么不推荐这种方式。

问题 1:依赖关系不明确,可读性差

字段注入通过 @Autowired 直接标记字段,从类的定义中无法直观看出依赖了哪些组件,必须深入代码内部才能了解。

字段注入(不推荐):

java 复制代码
@Service
public class UserService {
    // 依赖隐藏在字段中,不看具体代码不知道依赖了什么
    @Autowired
    private UserMapper userMapper;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    // 业务方法...
}

如果类有多个依赖,开发者需要逐个查看字段才能知道这个类依赖了哪些组件,不够直观。

构造器注入(推荐):

java 复制代码
@Service
public class UserService {
    private final UserMapper userMapper;
    private final RedisTemplate<String, Object> redisTemplate;
    
    // 依赖通过构造器参数明确声明,一目了然
    public UserService(UserMapper userMapper, RedisTemplate<String, Object> redisTemplate) {
        this.userMapper = userMapper;
        this.redisTemplate = redisTemplate;
    }
    
    // 业务方法...
}

通过构造器参数,一眼就能看出类依赖了 UserMapperRedisTemplate,依赖关系清晰。

问题 2:依赖可被篡改,线程不安全

字段注入的变量不能用 final 修饰(final 变量必须在构造时初始化),这意味着依赖可能在运行中被意外修改,存在线程安全风险。

字段注入的风险:

java 复制代码
@Service
public class UserService {
    @Autowired
    private UserMapper userMapper; // 非final,可被修改
    
    // 恶意代码或误操作可能修改依赖
    public void setUserMapper(UserMapper userMapper) {
        this.userMapper = userMapper; // 运行中被篡改
    }
}

如果依赖被意外替换,可能导致业务逻辑异常,且难以排查。

构造器注入的优势:

java 复制代码
@Service
public class UserService {
    private final UserMapper userMapper; // final修饰,不可修改
    
    public UserService(UserMapper userMapper) {
        this.userMapper = userMapper; // 仅在初始化时赋值
    }
}

final 保证依赖注入后无法被修改,符合 "不可变对象" 设计原则,线程更安全。

问题 3:单元测试困难

字段注入依赖 Spring 容器初始化时通过反射注入依赖,在单元测试中如果不启动 Spring 容器,需要手动通过反射设置依赖,非常繁琐。

字段注入的测试痛点:

java 复制代码
// 测试类
public class UserServiceTest {
    private UserService userService = new UserService(); // 直接new对象,依赖未注入
    
    @BeforeEach
    void setUp() throws Exception {
        // 必须通过反射手动设置依赖,代码繁琐
        Field userMapperField = UserService.class.getDeclaredField("userMapper");
        userMapperField.setAccessible(true);
        userMapperField.set(userService, mock(UserMapper.class)); // 注入mock对象
    }
}

构造器注入的测试优势:

java 复制代码
public class UserServiceTest {
    // 直接通过构造器传入mock依赖,无需反射
    private UserService userService = new UserService(mock(UserMapper.class));
    
    // 测试方法...
}

构造器注入允许直接通过构造方法传入 mock 对象,测试更简单,不依赖 Spring 容器。

问题 4:掩盖循环依赖

字段注入会让 Spring 容器可以 "容忍" 循环依赖(如 A 依赖 B,B 依赖 A),但循环依赖本身是代码设计的缺陷,应该避免。

字段注入导致的循环依赖(隐藏问题):

java 复制代码
@Service
public class AService {
    @Autowired
    private BService bService; // A依赖B
}

@Service
public class BService {
    @Autowired
    private AService aService; // B依赖A
}

Spring 能通过字段注入 "解决" 这种循环依赖,但这掩盖了设计问题,可能导致业务逻辑耦合过紧。

构造器注入暴露循环依赖:

如果用构造器注入,循环依赖会在项目启动时直接报错(BeanCurrentlyInCreationException),强迫开发者修复设计缺陷:

java 复制代码
@Service
public class AService {
    private final BService bService;
    
    public AService(BService bService) { // 构造器注入B
        this.bService = bService;
    }
}

@Service
public class BService {
    private final AService aService;
    
    public BService(AService aService) { // 构造器注入A,启动时直接报错
        this.aService = aService;
    }
}

总结

@Autowired 字段注入的核心问题是:隐藏依赖关系、允许依赖被修改、测试困难、掩盖设计缺陷 。而构造器注入通过显式声明依赖、支持 final 修饰、简化测试、暴露设计问题,更符合依赖注入的设计原则,因此被 Spring 官方和主流规范推荐。

实际开发中,结合 Lombok 的 @RequiredArgsConstructor 可以简化构造器注入的代码(自动生成含 final 字段的构造器),兼顾规范和开发效率。

3.反向判断

业务逻辑:1.状态正常的用户,2.余额充足,3.执行程序

可以使用反向判断,这样就不会出现if嵌套的情况。出现异常状态直接抛出异常即可。

java 复制代码
        // 2.判断用户状态
        if (user == null || user.getStatus() == 2) {
            throw new RuntimeException("用户状态异常");
        }
        // 3.判断用户余额
        if (user.getBalance() < money) {
            throw new RuntimeException("用户余额不足");
        }

2.3.3 Lambda查询和更新

在 MyBatis-Plus 中,lambdaQuery()lambdaUpdate()IService 接口提供的两个核心方法,分别用于基于 Lambda 表达式的查询操作更新操作

  • lambdaQuery() 用于查询操作 ,通过 实体类::get字段 构建 WHERE 条件,搭配 list()page() 等方法执行查询;
  • lambdaUpdate() 用于更新操作 ,通过 实体类::get字段 构建 WHERE 条件,通过 set() 方法设置更新内容,最后调用 update() 执行;
  • 类型安全 :通过 Lambda 表达式引用字段,编译期检查字段是否存在,避免字符串硬编码(如 "age")的拼写错误;
  • 简化代码 :无需手动创建 LambdaQueryWrapperLambdaUpdateWrapper,直接通过 service 链式调用;
2.3.3.1 lambdaQuery():基于 Lambda 的查询操作

快速创建 LambdaQueryWrapper 对象,通过 Lambda 表达式构建查询条件

**使用方法:**适用于所有查询场景(单条查询、列表查询、分页查询等),步骤如下:

  1. 调用 service.lambdaQuery() 获取 LambdaQueryWrapper 对象;
  2. 链式调用条件方法(如 eqgtlike 等),通过 实体类::get字段名 引用字段;
  3. 调用查询方法(如 list()one()page() 等)执行查询。
java 复制代码
@Service
public class UserServiceTest {
    @Autowired
    private UserService userService; // 继承IService<User>

    // 1. 查询列表:年龄>18且状态为1的用户
    public List<User> getAdultActiveUsers() {
        return userService.lambdaQuery()
                .gt(User::getAge, 18)       // WHERE age > 18
                .eq(User::getStatus, 1)     // AND status = 1
                .list();                    // 执行查询,返回列表
    }

    // 2. 单条查询:查询用户名=admin的用户
    public User getAdminUser() {
        return userService.lambdaQuery()
                .eq(User::getUsername, "admin")  // WHERE username = 'admin'
                .one();                          // 返回第一条记录(或null)
    }

    // 3. 分页查询:查询余额在100-1000的用户,分页第1页(每页20条)
    public IPage<User> getBalancePage() {
        Page<User> page = new Page<>(1, 20); // 第1页,每页20条
        return userService.lambdaQuery()
                .between(User::getBalance, 100, 1000)  // WHERE balance BETWEEN 100 AND 1000
                .orderByDesc(User::getCreateTime)       // ORDER BY create_time DESC
                .page(page);                           // 执行分页查询
    }
}
2.3.3.2 lambdaUpdate():基于 Lambda 的更新操作

快速创建 LambdaUpdateWrapper 对象,通过 Lambda 表达式构建更新条件和更新内容

**使用方法:**适用于所有更新场景(单条更新、批量更新等),步骤如下:

  1. 调用 service.lambdaUpdate() 获取 LambdaUpdateWrapper 对象;
  2. 链式调用条件方法(如 eqin 等)指定更新范围(WHERE 条件);
  3. 调用 set 方法通过 实体类::set字段名 指定更新内容(SET 语句);
  4. 调用 update() 执行更新。
java 复制代码
@Service
public class UserServiceTest {
    @Autowired
    private UserService userService;

    // 1. 单条更新:将ID=1的用户余额+100
    public boolean addBalanceToUser1() {
        return userService.lambdaUpdate()
                .eq(User::getId, 1)                 // WHERE id = 1
                .set(User::getBalance, User::getBalance + 100)  // SET balance = balance + 100
                .update();                          // 执行更新
    }

    // 2. 批量更新:将状态为0的用户的邮箱设为"inactive@example.com"
    public boolean updateInactiveUsers() {
        return userService.lambdaUpdate()
                .eq(User::getStatus, 0)             // WHERE status = 0
                .set(User::getEmail, "inactive@example.com")  // SET email = 'inactive@example.com'
                .update();
    }

    // 3. 条件更新:年龄>50的用户,状态改为2(退休)
    public boolean updateRetiredUsers() {
        return userService.lambdaUpdate()
                .gt(User::getAge, 50)               // WHERE age > 50
                .set(User::getStatus, 2)            // SET status = 2
                .update();
    }
}

2.3.4 批量新增

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

java 复制代码
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合并为一条,像这样:

java 复制代码
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语句。

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

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

java 复制代码
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

mp批处理:

java 复制代码
@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));
}

三. 扩展功能

3.1 代码生成器

3.2 DB静态工具

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

关于Db静态工具在使用方面,和IService基本一致 ,只是需要在调用时向它传一个字节码文件,用来指定泛型。

使用Db静态工具是为了避免出现相互依赖的情况

改造根据id查询用户的接口,查询用户的同时,查询出用户对应的所有地址:

java 复制代码
    @GetMapping("/{id}")
    @ApiOperation(value = "根据id查询用户接口")
    private UserVO queryUserById(@ApiParam("用户ID") @PathVariable Long id){
        return userService.queryUserAndAddressById(id);
    }
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);
        //3.2 地址转VO
        if (CollUtil.isNotEmpty(addresses)){
            userVO.setAddress(BeanUtil.copyToList(addresses, AddressVO.class));
        }
        return userVO;
    }

实现根据用户id查询收货地址功能,需要验证用户状态,冻结用户抛出异常:

java 复制代码
    @GetMapping("/{userId}")
    @ApiOperation(value = "根据用户id查询地址信息")
    public List<AddressVO> list(@PathVariable Long userId){
        return addressService.listByUserId(userId);
    }
java 复制代码
    @Override
    public List<AddressVO> listByUserId(Long userId) {
        User user = Db.getById(userId,User.class);
        if (user == null || user.getStatus() == 2) {
            throw new RuntimeException("用户状态异常");
        }
        return BeanUtil.copyToList(lambdaQuery().eq(Address::getUserId,userId).list(),AddressVO.class);
    }

根据id批量查询用户的接口,查询用户的同时,查询出用户对应的所有地址

java 复制代码
    @GetMapping
    @ApiOperation(value = "根据id批量查询用户接口")
    private List<UserVO> queryBatchUserById(@ApiParam("用户ID集合") @RequestParam("ids") List<Long> ids){
        return userService.queryUsersAndAddressById(ids);
    }

通过userIds集合查询地址集合,再通过groupingBy以userId为条件讲地址集合分组,最后遍历UserVO集合,将对应id的地址添加进VO;

而非获取用户集合后,遍历用户集合根据id查询地址;这样写关于查询地址部分的sql语句是一条一条的,性能差,应该一次查出来再进行处理。

java 复制代码
    @Override
    public List<UserVO> queryUsersAndAddressById(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, ids).list();
        //2.3 转地址VO
        List<AddressVO> addressVOList = BeanUtil.copyToList(addresses,AddressVO.class);
        //2.4 根据用户id分组
        Map<Long, List<AddressVO>> addressMap = new HashMap<>();
        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转VO
            UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
            userVO.setAddress(addressMap.get(userVO.getId()));
            list.add(userVO);
        }
        return list;
    }

新知识:stream流分类。

java 复制代码
List<Long> userIds = users.stream().map(User::getId).collect(Collectors.toList());

.stream()开启stream流,.map(User::getId)指定需要的字段,.collect(Collectors.toList())打包成集合。

java 复制代码
Map<Long, List<AddressVO>> addressMap = new HashMap<>();
        if (CollUtil.isNotEmpty(addressVOList)){
            addressMap = addressVOList.stream().collect(Collectors.groupingBy(AddressVO::getUserId));
        }

使用groupingBy,指定分组依据;最终Map的key值就是分类依据,即userId。

3.3 逻辑删除

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

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

一旦采用了逻辑删除,所有的查询和删除逻辑都要跟着变化,非常麻烦。

为了解决这个问题,MybatisPlus就添加了对逻辑删除的支持。

注意,只有MybatisPlus生成的SQL语句才支持自动的逻辑删除,自定义SQL需要自己手动处理逻辑删除。

给address表中增加deleted字段,同时实体类上也要加上该字段。

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

java 复制代码
mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: deleted # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
java 复制代码
@SpringBootTest
class IAddressServiceTest {
    @Autowired
    private IAddressService addressService;
    @Test
    void testLogDelete() {
        addressService.removeById(70L);
        Address address = addressService.getById(60L);
        System.out.println("address = "+address);
    }
}

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

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

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

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

3.4 枚举处理器

实体类中某些状态码如果直接使用数字去描述,当状态数量过多后不便于阅读与理解,所以对于状态字段适用枚举类来描述,将会便于理解。

但适用枚举类会存在一个问题,实体类中字段是枚举类、而数据库中字段是int类型,因此业务操作时必须手动把枚举Integer转换,非常麻烦。

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

实现过程:

1.编写枚举类

2.将实体类中对应字段类型替换为枚举类

3.要让MybatisPlus处理枚举与数据库类型自动转换,在对应枚举类字段上加@EnumValue注解

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

java 复制代码
mybatis-plus:
  configuration:
    default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler

枚举类:

java 复制代码
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;
    }
}

测试结果:

其中status显示的是枚举类中的desc,这是因为我们在枚举类中的desc字段上添加了@JsonValue注解。

如果不添加注解,由于SpringMVC底层在处理JSON数据时,使用Jackson作为JSON处理器,他默认返回枚举类的名称,即"NORMAL/FREEZE",要想展示其他字段,如value/desc,在对应字段上添加@JsonValue即可。

3.5 JSON处理器

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

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

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

实现过程:

1.定义实体类匹配Json字符串

2.将User类的info字段修改为UserInfo类型,并声明类型处理器

同时,在User类上添加一个注解,声明自动映射

处理器使用Jackson类型的,因为这和SpringMVC底层一致;设置autoResultMap为true。

java 复制代码
@Data
@TableName(value = "tb_user", autoResultMap = true)
public class User {
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @TableField("`username`")
    private String username;
    private String password;
    @TableField("`phone`")
    private String phone;
    @TableField(typeHandler = JacksonTypeHandler.class)
    private UserInfo info;
    private UserStatus status;
    private Integer balance;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

3.将User和UserVO类的info字段类型修改为UserInfo

测试结果:

四. 插件功能

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

  • PaginationInnerInterceptor:自动分页

  • TenantLineInnerInterceptor:多租户

  • DynamicTableNameInnerInterceptor:动态表名

  • OptimisticLockerInnerInterceptor:乐观锁

  • IllegalSQLInnerInterceptor:sql 性能规范

  • BlockAttackInnerInterceptor:防止全表更新与删除

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

  • 多租户,动态表名

  • 分页,乐观锁

  • sql 性能规范,防止全表更新与删除

这里我们以分页插件为里来学习插件的用法。

4.1.分页插件

在未引入分页插件的情况下,MybatisPlus是不支持分页功能的,IServiceBaseMapper中的分页方法都无法正常起效。 所以,我们必须配置分页插件。

在项目中新建一个配置类:

向拦截器中添加插件。

java 复制代码
@Configuration
public class MyBatisConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        //1. 创建分页插件
        PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
        paginationInnerInterceptor.setMaxLimit(1000L);
        //2.添加分页插件
        interceptor.addInnerInterceptor(paginationInnerInterceptor);
        return interceptor;
    }
}

编写一个分页查询的测试:

java 复制代码
    @Test
    void testPageQuery(){
        int pageNo = 1;
        int pageSize = 5;
        //1.准备分页条件
        //1.分页条件
        Page<User> page = Page.of(pageNo,pageSize);
        //1.2 排序条件
        page.addOrder(new OrderItem("balance",true));
        page.addOrder(new OrderItem("id",true));
        //2.分页查询
        Page<User> p = userService.page(page);
        //3.解析
        long total = p.getTotal();
        System.out.println("total = "+total);
        long pages = p.getPages();
        System.out.println("pages = "+pages);
        List<User> users = page.getRecords();
        users.forEach(System.out::println);
    }

运行的SQL如下:


4.2.通用分页实体

现在要实现一个用户分页查询的接口,接口规范如下:

实现过程:

1.创建请求参数实体:

由于请求参数字段很多,所以这里单独设计一个实体用来接收前端请求数据会更好理解;同时可以预测每个分页请求都需要设计一个实体,但是他们都存在共同的字段**"页码"和"每页条数"** ,所以设计一个共同的分页请求实体 ,再设计一个剩余字段的实体去继承分页请求实体,这样会便于使用与修改。

分页请求实体:

java 复制代码
@Data
@ApiModel("分页查询实体")
public class PageQuery {
    @ApiModelProperty("页码")
    private Integer pageNo;
    @ApiModelProperty("每页条数")
    private Integer pageSize;
    @ApiModelProperty("排序字段")
    private String sortBy;
    @ApiModelProperty("是否升序")
    private Boolean isAsc;
}

剩余字段实体:

java 复制代码
@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;
}

注:分页请求实体中"排序字段"和"是否升序"两个字段并不是必须的。

2.设计分页响应实体:

通过观察响应Json串,发现一共有三个字段,"总数"、"页码"、"当页数据",前两者可以直接使用Long或Integer类型,当页数据集合可以使用<T>,当使用时再规定类型。

分页响应实体:

java 复制代码
@Data
@ApiModel(description = "分页结果")
public class PageDTO<T> {
    @ApiModelProperty("总条数")
    private Long total;
    @ApiModelProperty("总页数")
    private Long pages;
    @ApiModelProperty("数据集合")
    private List<T> list;
}

3.在Service实现分页查询

  • 使用Page构造分页条件
  • 使用addOrder构造排序条件
  • 使用lambdaQuery().page获得分页完成后的Page对象
  • 封装数据并返回
java 复制代码
    @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));
        }
        //2.分页查询
        Page<User> p = lambdaQuery()
                .like(name != null, User::getUsername, name)
                .eq(status != null, User::getStatus, status)
                //.one():一个;.list():集合;.count():总数;.page():分页;.exist():是否存在
                .page(page);
        //3.封装VO结果
        PageDTO<UserVO> dto = new PageDTO<>();
        //3.1总条数
        Long total = p.getTotal();
        //3.2总页数
        Long pages = p.getPages();
        //3.3当前页数据
        List<UserVO> list = BeanUtil.copyToList(p.getRecords(), UserVO.class);
        //4.返回
        dto.setTotal(total);
        dto.setPages(pages);
        dto.setList(list);
        return dto;
    }

4.3 通用分页实体与MP转换

在刚才的代码中,从PageQueryMybatisPlusPage之间转换的过程还是比较麻烦的。

我们完全可以在PageQuery这个实体中定义一个工具方法,简化开发。

实现了使用PageQuery对象调用queryToMP(排序条件),可以直接获得Page对象,供LambdaQuery.page()使用。

java 复制代码
@Data
@ApiModel("分页查询实体")
public class PageQuery {
    @ApiModelProperty("页码")
    private Integer pageNo=1;
    @ApiModelProperty("每页条数")
    private Integer pageSize=5;
    @ApiModelProperty("排序字段")
    private String sortBy;
    @ApiModelProperty("是否升序")
    private Boolean isAsc=false;

    public <T> Page<T> queryToMP(OrderItem ... items){
        //1分页条件
        Page<T> page = Page.of(pageNo, pageSize);
        //2排序条件
        if (StrUtil.isNotBlank(sortBy)){
            //不为空
            page.addOrder(new OrderItem(sortBy,isAsc));
        }else if(items != null){
            //为空,按照默认更新时间排序
            page.addOrder(items);
        }else {
            page.addOrder(new OrderItem("update_time",false));
        }
        return page;
    }

    public <T> Page<T> queryToMPByUpdateTime(){
        return queryToMP(new OrderItem("update_time",false));
    }
    public <T> Page<T> queryToMPByCreateTime(){
        return queryToMP(new OrderItem("create_time",false));
    }
    public <T> Page<T> queryToMP(String defaultSortBy,Boolean isAsc){
        return this.queryToMP(new OrderItem(defaultSortBy,isAsc));
    }

}

在查询出分页结果后,数据的非空校验,数据的vo转换都是模板代码,编写起来很麻烦。

我们完全可以将其封装到PageDTO的工具方法中,简化整个过程。

实现了将查询完毕后的Page对象转换为PageDTO<VO>对象,使用时需要传VO的字节码文件;或自定义转换流程。

java 复制代码
@Data
@ApiModel(description = "分页结果")
@AllArgsConstructor
@NoArgsConstructor
public class PageDTO<T> {
    @ApiModelProperty("总条数")
    private Long total;
    @ApiModelProperty("总页数")
    private Long pages;
    @ApiModelProperty("数据集合")
    private List<T> list;

    public static <V, P> PageDTO<V> empty(Page<P> p){
        return new PageDTO<>(p.getTotal(), p.getPages(), Collections.emptyList());
    }

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

    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 = p.getRecords().stream().map(convertor).collect(Collectors.toList());
        // 3.封装返回
        return new PageDTO<>(p.getTotal(), p.getPages(), vos);
    }

}

使用两者工具方法修改后的分页查询Service层:

java 复制代码
    @Override
    public PageDTO<UserVO> queryUsersPage(UserQuery query) {
        String name = query.getName();
        Integer status = query.getStatus();
        //1.规定分页条件,获取page对象
        Page<User> page = query.queryToMP();

        //2.分页查询
        Page<User> p = lambdaQuery()
                .like(name != null, User::getUsername, name)
                .eq(status != null, User::getStatus, status)
                //.one():一个;.list():集合;.count():总数;.page():分页;.exist():是否存在
                .page(page);

        PageDTO<UserVO> dto = PageDTO.of(p, user -> {
            //1.拷贝
            UserVO vo = BeanUtil.copyProperties(user, UserVO.class);
            //2.特殊操作
            String username = vo.getUsername();
            vo.setUsername(username.substring(0, username.length() - 2) + "**");
            return vo;
        });
        return dto;
    }

注意:这种直接在实体中编写工具方法的操作耦合性强不建议使用,建议单独编写工具类,并指明是底层使用的是哪个工具,比如MP。

相关推荐
灰灰老师2 小时前
在Ubuntu22.04和24.04中安装Docker并安装和配置Java、Mysql、Tomcat
java·mysql·docker·tomcat
235162 小时前
【MQ】RabbitMQ:架构、工作模式、高可用与流程解析
java·分布式·架构·kafka·rabbitmq·rocketmq·java-rabbitmq
Porunarufu2 小时前
JAVA·类和对象③封装及包
java·开发语言
霍小毛3 小时前
Kubernetes云平台管理实战:滚动升级与秒级回滚
java·容器·kubernetes
代码充电宝3 小时前
LeetCode 算法题【简单】20. 有效的括号
java·算法·leetcode·面试·职场和发展
祈祷苍天赐我java之术3 小时前
Redis 的原子性操作
java·redis
wdfk_prog3 小时前
klist 迭代器初始化:klist_iter_init_node 与 klist_iter_init
java·前端·javascript
凸头3 小时前
Collections.synchronizedList()详解
java
用户0273851840263 小时前
【Android】MotionLayout详解
java·程序员