系列博客目录
文章目录
- 系列博客目录
- Part1:条件构造器
-
- [案例 基于QueryWrapper的查询](#案例 基于QueryWrapper的查询)
- [案例 基于UpdateWrapper的查询](#案例 基于UpdateWrapper的查询)
- 条件构造器的用法总结
- Part2:自定义SQL
- Part3:IService接口
Part1:条件构造器
之前的不足:之前的入门案例中,为了简化学习,演示的增删改查都是根据ID进行的,但是真是业务开发中,增删改查的条件很复杂,MP提供条件构造器,帮助我们构建条件。
MyBatisPlus支持各种复杂的where条件,可以满足日常开发的所有需求。下面给出的方法,也是来着BaseMapper
,但是他不再用Id
当作参数了,而是Wrapper类型
的参数,Wrapper
就是条件构造器,就是用来构建复杂Sql语句的。
Wrapper是最顶级的父类,还有很多继承它的。
比如上图中第二层的AbstractWrapper
,从下图可以看出,Sql语句中where条件
中写过的条件的很多在这里都有对应的方法 ,比如eq
就是"="
。
从上上图可以看到,AbstractWrapper
还有几个儿子,也就是进行了继承以及拓展。QueryWrapper
就是拓展了查询相关功能,比如查询时候加了条件,指定select哪些字段(如下图所示)。UpdateWrapper
就是拓展了更新的相关功能。
从上上上图中左下角可以看到,有三个类,只比之前讲过的三个多了Lambda,和之前的区别就是构建条件的时候,使用了Lambda的语法。
案例 基于QueryWrapper的查询
需求:
- 查询出名字中带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);
}
运行结果:
- 更新用户名为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);
}
条件构造器的用法总结
- QueryWrapper和LambdaQueryWrapper通常用来构建selectdelete、update的where条件部分
- UpdateWrapper和LambdaUpdateWrapper通常只有在set语句比较特殊才使用
- 尽量使用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
,那他就更新,不带,他就新增。
removeByIds
和 removeBatchByIds
(以及类似对比)都是常见的数据库操作方法,尤其在使用 MyBatis 或者类似的 ORM 框架时。这两者的主要区别通常体现在方法的命名和具体实现细节上,但在某些框架中,它们可以执行相似的操作。具体来说:
-
removeByIds
:- 一般用于删除指定的 单个或多个 记录。
- 该方法通常接收一个集合或数组作为参数,可以根据提供的 ID 列表删除对应的记录。
- 适用于较简单的删除操作,可能适用于删除一个 ID 列表中的所有记录。
-
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接口基本用法的使用流程是怎样的?
- 自定义
Service
接口,继承IService
接口:
java
public interface IUserService extends IService<User> { //User 是实体类。
// 你可以在这里定义自定义的方法
}
- 自定义
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中对LambdaQueryWrapper
和LambdaUpdateWrapper
的用法进一步做了简化。我们无需自己通过new
的方式来创建Wrapper,而是直接调用lambdaQuery
和lambdaUpdate
方法:
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();
}