黑马微服务开发与实战学习笔记_MybatisPlus_P2核心功能

系列博客目录


文章目录


Part1:条件构造器

之前的不足:之前的入门案例中,为了简化学习,演示的增删改查都是根据ID进行的,但是真是业务开发中,增删改查的条件很复杂,MP提供条件构造器,帮助我们构建条件。

MyBatisPlus支持各种复杂的where条件,可以满足日常开发的所有需求。下面给出的方法,也是来着BaseMapper,但是他不再用Id当作参数了,而是Wrapper类型的参数,Wrapper就是条件构造器,就是用来构建复杂Sql语句的。

Wrapper是最顶级的父类,还有很多继承它的。

比如上图中第二层的AbstractWrapper,从下图可以看出,Sql语句中where条件中写过的条件的很多在这里都有对应的方法 ,比如eq就是"="

从上上图可以看到,AbstractWrapper还有几个儿子,也就是进行了继承以及拓展。QueryWrapper就是拓展了查询相关功能,比如查询时候加了条件,指定select哪些字段(如下图所示)。UpdateWrapper就是拓展了更新的相关功能。

从上上上图中左下角可以看到,有三个类,只比之前讲过的三个多了Lambda,和之前的区别就是构建条件的时候,使用了Lambda的语法。

案例 基于QueryWrapper的查询

需求:

  1. 查询出名字中带o的,存款大于等于1000元的人的id、username、info、balance字段。
sql 复制代码
SELECT id,username,info,balance
FROM user
WHERE username LIKE ? AND balance >= ?

我们可以通过代码自动补全中给我们的信息来知道,哪个函数使用了Wrapper,哪个函数返回什么类型,通过这两点来确定我们使用哪个函数。

java 复制代码
@Test
void testQueryWrapper(){
	//构建查询条件,实现不通过Id查询
	QueryWrapper<User> wrapper = new QueryWrapper<User>()
         .select("id","username","info","balance")
                .like("username","o")
                      .ge("balance",1000);
    //实现查询
    List<User> users = userMapper.selectList(wrapper);
    users.forEach(System.out::println);
}

运行结果:

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

案例 基于UpdateWrapper的查询

需求:更新id为1,2,4的用户的余额,扣200。这种更新比较特殊。每个用户扣完之后金额不同,不能像之前一样直接写死。

sql 复制代码
UPDATE user
	SET balance = balance - 200 
	WHERE id in (1,2,4)
java 复制代码
@Test
void testUpdateWrapper(){
    List<Long> ids = List.of(1L,2L,4L);
    UpdateWrapper<User> wrapper = new UpdateWrapper<User>()
            .setSql("balance = balance - 200")
                    .in("id", ids);
    userMapper.update(null, wrapper); //更新的内容不需要填了,直接在Wrapper中写
}

MP中除了新增,删改查都可以通过Wrapper进行操作。
LambdaWrapper 与 一般Wrapper的区别,是在构造条件时候需要使用Lambda表达式,因为目前我们都是硬编码,字段名都是写死的(比如select("id","username","info","balance")),使用lambda表达式可以解决这个问题。

示例:结果与之前一样,推荐使用Lambda表达式,避免硬编码

java 复制代码
void testLambdaQueryWrapper() {
    // 构建查询条件
    LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>()
        .select(User::getId, User::getUsername, User::getInfo, User::getBalance)  // 查询字段
        .like(User::getUsername, "o")  // 用户名包含 "o"
        .ge(User::getBalance, 1000);   // 余额大于等于 1000
    
    //实现查询
    List<User> users = userMapper.selectList(wrapper);
    users.forEach(System.out::println);
}

条件构造器的用法总结

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

Part2:自定义SQL

为什么有了MP实现自动的增删改查,以及条件构造器了,还要自定义SQL?通过两个案例解释一下。

案例1

需求:将id在指定范围的用户(例如1、2、4)的余额扣减指定值。手写sql代码如下。

xml 复制代码
<update id="updateBalanceByIds">
    UPDATE user
    SET balance = balance - #{amount}
    WHERE id IN
    <foreach collection="ids" separator="," item="id" open="(" close=")">
        #{id}
    </foreach>
</update>

我们之前想要实现更新,需要编写如上的sql语句,虽然Wrapper可以更加简单的实现,但是Wrapper通过setSql实现拼接sql代码的时候,实际上是在业务代码中编写了Sql代码,这在大公司中是不允许的,一般只允许在xml中编写Sql代码。

案例2

那我们就只能被迫手写所有Sql代码了吗,即使如果sql语句很复杂其实用MP可以很轻松的编写的话呢?不光是更新,在查询中,如下面代码所示,MP其实可以不管where条件多复杂都可以快速的编写出来,但是sql语句的前半部分就不一定了,前面的SELECT不是正常在数据表中字段了,还要起别名,那这时候如果用Wrapper,就只能通过setSql()拼出来。

xml 复制代码
<select id="selectStatusCount" resultType="Map">
    SELECT status, COUNT(id) AS total
    FROM tb_user
    <where>
        <if test="name != null">AND username LIKE #{name}</if>
        <if test="uids != null">
            AND id IN
            <foreach collection="uids" open="(" close=")" item="id" separator=",">
                #{id}
            </foreach>
        </if>
    </where>
    GROUP BY status
</select>

解决方案

有无两全之策?那就是自定义Sql ,我们可以利用MyBatisPlus的Wrapper来构建复杂的Where条件,然后自己定义SOL语句中剩下的部分。

既保证了不在业务层编写Sql,遵循了企业规范,同时享受到了MP生成Sql条件的便捷特性。

Part3:IService接口

IService接口基本用法

前面我们感受到了继承是很爽的,为了一直继承一直爽,MP为我们提供了IService接口,继承之后,一些增删改查的service代码也不用写了。比MapperBase只多不少。

左上角的save 是新增,第一个是新增一个,第二个接收的是一个集合,即批量新增。之前实现批量新增,需要在Mapper.xml文件里写foreach循环组装一堆数据,现在不用写了,有了这个接口,直接调用即可。比如saveOrUpdate,如果给的数据带着Id,那他就更新,不带,他就新增。

removeByIdsremoveBatchByIds (以及类似对比)都是常见的数据库操作方法,尤其在使用 MyBatis 或者类似的 ORM 框架时。这两者的主要区别通常体现在方法的命名和具体实现细节上,但在某些框架中,它们可以执行相似的操作。具体来说:

  1. removeByIds

    • 一般用于删除指定的 单个或多个 记录。
    • 该方法通常接收一个集合或数组作为参数,可以根据提供的 ID 列表删除对应的记录。
    • 适用于较简单的删除操作,可能适用于删除一个 ID 列表中的所有记录。
  2. removeBatchByIds

    • 通常是 批量删除 的语义,意味着该方法在数据库中执行的是一个批量删除操作,通常会涉及数据库性能优化(如批量执行 SQL)。
    • 在某些框架中,removeBatchByIds 可能会采用不同的数据库操作方式来提高性能
    • 如果删除的是大量数据,使用 removeBatchByIds 可能会有更好的性能表现,因为它优化了执行流程。

总结:

  • removeByIds 适用于简单的删除操作,通常是删除一个或多个 ID 对应的记录。
  • removeBatchByIds 侧重于批量删除,通常会优化批量删除的性能。

虽然在某些框架中,这两个方法的实现方式可能相似,且可以互换使用,但通常 removeBatchByIds 强调了处理大量数据时的性能优化。如果数据操作达到几千个,用Batch性能好一点。

大多数时候我们根据Id进行操作,但遇到复杂条件,我们要用Wrapper,为了传Wrapper,我们就要new Wrapper,这样的话就繁琐了一点,所以service就提供了lambdaQuery()的方法,可以得到lambdaQuery()的chain的Wrapper,也就是链式编程的Wrapper,也就是说用它可以直接基于lambdaQuery()做查询了,不用自己去new了,更加方便了。遇到一般用Id的就用普通方法,遇到复杂的就用Lambda的。
注意 我们现在修改之前的业务层,用之前业务层的接口继承IService接口,但是我们知道,我们需要把业务层的接口所定义的函数都实现,那我现在要把IService中所有的方法都实现吗?其实MP不光给我们提供了IService接口,还提供了IService默认的实现类ServiceImpl。

一句话:接口继承接口,实现类继承实现类。

MP的Service接口基本用法的使用流程是怎样的?

  1. 自定义 Service 接口,继承 IService 接口:
java 复制代码
public interface IUserService extends IService<User> { //User 是实体类。
    // 你可以在这里定义自定义的方法
}
  1. 自定义 Service 实现类,实现自定义接口并继承 ServiceImpl 类:
java 复制代码
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
										//UserMapper继承BaseMapper
    // 你可以在这里实现 IUserService 接口中的自定义方法
}

项目结构如下:

为了测试一下:

我们找到我们定义的接口IUserService,鼠标悬停在接口名代码上,Alt+Enter,创建测试IUserServiceTest.java

java 复制代码
@SpringBootTest
class IUserServiceTest {
    @Autowired
    private IUserService userService;

    @Test
    void testSaveUser(){
        User user = new User();
        user.setId(5L);
        user.setUsername("LiLei");
        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(List.of(1L, 2L, 4L));
        users.forEach(System.out::println);
    }

}

实战案例

编号 接口描述 请求方式 请求路径 请求参数 返回值
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, 扣减金额

为了做测试 做点准备工作

准备工作

引入几个依赖

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

配置swagger信息(直接复制到application.yaml):

yaml 复制代码
knife4j:
  enable: true
  openapi:
    title: 用户管理接口文档
    description: "用户管理接口文档"
    email: zhanghuyi@itcast.cn
    concat: 虎哥
    url: https://www.itcast.cn
    version: v1.0.0
    group:
      default:
        group-name: default
        api-rule: package
        api-rule-resources:
          - com.itheima.mp.controller

然后,接口需要两个实体:

  • UserFormDTO:代表新增时的用户表单 (Data Transfer Object 数据传输对象)
  • UserVO:代表查询的返回结果 (Value Object 值对象)

首先是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;
}

然后是UserVO:与一般实体User不同在于,比如相对于数据库表中的字段,这里不涉及密码,因为不想把密码从数据库中读出

java 复制代码
package com.itheima.mp.domain.vo;

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

@Data
@ApiModel(description = "用户VO实体")
public class UserVO {
    
    @ApiModelProperty("用户id")
    private Long id;
    
    @ApiModelProperty("用户名")
    private String username;
    
    @ApiModelProperty("详细信息")
    private String info;

    @ApiModelProperty("使用状态(1正常 2冻结)")
    private Integer status;
    
    @ApiModelProperty("账户余额")
    private Integer balance;
}

开始基本用法实战

创建UserController.java

java 复制代码
package com.itheima.mp.controller;

import cn.hutool.core.bean.BeanUtil;
import com.itheima.mp.domain.dto.UserFormDTO;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.domain.vo.UserVO;
import com.itheima.mp.service.IUserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@Api(tags = "用户管理接口")
@RequiredArgsConstructor //Lombok提供的一个注解。Lombok会为类自动生成一个构造函数,构造函数的参数包括所有声明为 final 的字段和使用 @NonNull 注解的非 final 字段。为下面的userService 我们没有用@Autowired注入,使用Spring推荐的构造函数注入 
@RestController
@RequestMapping("users")
public class UserController {

    private final IUserService userService;//一个类的成员变量很多,怎样让Lombok知道哪些我们想让它帮我们注入呢,加个final,final修饰成员变量创建类时必须初始化。

    @PostMapping
    @ApiOperation("新增用户")
    public void saveUser(@RequestBody UserFormDTO userFormDTO){//@RequestBody 接收JSON格式
        // 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);
    }
}

可以看到上述接口都直接在controller即可实现,无需编写任何service代码,非常方便。

不过,一些带有业务逻辑的接口则需要在service中自定义实现了。例如下面的需求:

  • 根据id扣减用户余额

这看起来是个简单修改功能,只要修改用户余额即可。但这个业务包含一些业务逻辑处理:

  • 判断用户状态是否正常
  • 判断用户余额是否充足

这些业务逻辑都要在service层来做,另外更新余额需要自定义SQL,要在mapper中来实现。因此,我们除了要编写controller以外,具体的业务还要在service和mapper中编写。

首先在UserController.java中定义一个方法:

java 复制代码
@PutMapping("{id}/deduction/{money}")
@ApiOperation("扣减用户余额")
public void deductBalance(@PathVariable("id") Long id, @PathVariable("money")Integer money){
    userService.deductBalance(id, money);
}

然后是UserService接口(注意这里是从业务层方面说叫UserService接口,代码方面名叫IUserService ):

java 复制代码
package com.itheima.mp.service;

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

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

最后是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);
    }
}

最后是mapper:

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

IService的Lambda查询实战

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

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

可以理解成一个用户的后台管理界面,管理员可以自己选择条件来筛选用户,因此上述条件不一定存在,需要做判断。

一般的语法:

xml 复制代码
<select id="queryUsers" resultType="com.itheima.mp.domain.po.User">
    SELECT *
    FROM tb_user
    <where>
        <if test="name != null">
            AND username LIKE #{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中定义一个controller方法:

java 复制代码
@GetMapping("/list")
@ApiOperation("根据id集合查询用户")
public List<UserVO> queryUsers(UserQuery query){
    // 1.组织条件
    String username = query.getName();
    Integer status = query.getStatus();
    Integer minBalance = query.getMinBalance();
    Integer maxBalance = query.getMaxBalance();
    LambdaQueryWrapper<User> wrapper = new QueryWrapper<User>().lambda()
            .like(username != null, User::getUsername, username)
            .eq(status != null, User::getStatus, status)
            .ge(minBalance != null, User::getBalance, minBalance)
            .le(maxBalance != null, User::getBalance, maxBalance);
    // 2.查询用户
    List<User> users = userService.list(wrapper);
    // 3.处理vo
    return BeanUtil.copyToList(users, UserVO.class);
}

在组织查询条件的时候,我们加入了 username != null 这样的参数,意思就是当条件成立时才会添加这个查询条件,类似Mybatis的mapper.xml文件中的<if>标签。这样就实现了动态查询条件效果了。

不过,上述条件构建的代码太麻烦了。

因此Service中对LambdaQueryWrapperLambdaUpdateWrapper的用法进一步做了简化。我们无需自己通过new的方式来创建Wrapper,而是直接调用lambdaQuerylambdaUpdate方法:

java 复制代码
@GetMapping("/list")
@ApiOperation("根据id集合查询用户")
public List<UserVO> queryUsers(UserQuery query){
    // 1.组织条件
    String username = query.getName();
    Integer status = query.getStatus();
    Integer minBalance = query.getMinBalance();
    Integer maxBalance = query.getMaxBalance();
    // 2.查询用户
    List<User> users = userService.lambdaQuery()
            .like(username != null, User::getUsername, username)
            .eq(status != null, User::getStatus, status)
            .ge(minBalance != null, User::getBalance, minBalance)
            .le(maxBalance != null, User::getBalance, maxBalance)
            .list();
    // 3.处理vo
    return BeanUtil.copyToList(users, UserVO.class);
}

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

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

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

与lambdaQuery方法类似,IService中的lambdaUpdate方法可以非常方便的实现复杂更新业务。例如下面的需求:

需求:改造根据id修改用户余额的接口,要求如下

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

也就是说我们在扣减用户余额时,需要对用户剩余余额做出判断,如果发现剩余余额为0,则应该将status修改为2,这就是说update语句的set部分是动态的。

java 复制代码
@Override
@Transactional
public void deductBalance(Long id, Integer money) {
    // 1.查询用户
    User user = getById(id);
    // 2.校验用户状态
    if (user == null || user.getStatus() == 2) {
        throw new RuntimeException("用户状态异常!");
    }
    // 3.校验余额是否充足
    if (user.getBalance() < money) {
        throw new RuntimeException("用户余额不足!");
    }
    // 4.扣减余额 update tb_user set balance = balance - ?
    int remainBalance = user.getBalance() - money;
    lambdaUpdate()
            .set(User::getBalance, remainBalance) // 更新余额
            .set(remainBalance == 0, User::getStatus, 2) // 动态判断,是否更新status
            .eq(User::getId, id)
            .eq(User::getBalance, user.getBalance()) // 乐观锁
            .update();
}

批量新增

相关推荐
myNameGL23 分钟前
linux安装idea
java·ide·intellij-idea
青春男大25 分钟前
java栈--数据结构
java·开发语言·数据结构·学习·eclipse
HaiFan.1 小时前
SpringBoot 事务
java·数据库·spring boot·sql·mysql
我要学编程(ಥ_ಥ)1 小时前
一文详解“二叉树中的深搜“在算法中的应用
java·数据结构·算法·leetcode·深度优先
music0ant1 小时前
Idea 添加tomcat 并发布到tomcat
java·tomcat·intellij-idea
mashagua1 小时前
RPA系列-uipath 学习笔记3
笔记·学习·rpa
nikoni231 小时前
828考研资料汇总
笔记·其他·硬件工程
计算机徐师兄2 小时前
Java基于SSM框架的无中介租房系统小程序【附源码、文档】
java·微信小程序·小程序·无中介租房系统小程序·java无中介租房系统小程序·无中介租房微信小程序
源码哥_博纳软云2 小时前
JAVA智慧养老养老护理帮忙代办陪诊陪护小程序APP源码
java·开发语言·微信小程序·小程序·微信公众平台
沐泽Mu2 小时前
嵌入式学习-QT-Day05
开发语言·c++·qt·学习