条件构造器
MyBatisPlus支持各种复杂的where条件, 可以满足日常开发的所有需求
Wrapper 条件构造器专门用于 构造复杂的查询条件
- BaseMapper提供很多增删改查的方法, 其中的Wrapper就是用于构建复杂查询条件的
Wrapper的继承体系
- Wrapper是最顶级的父类
- AbstractWrapper继承了Wrapper, 定义了很多用于复杂条件查询的方法, 非常重要
- QueryWrapper和UpdateWrapper则是进一步扩展了AbstractWrapper的功能
- QueryWrapper扩展了查询语句的select 部分, 可以灵活的指定返回的字段, 默认是查询全部字段
- UpdateWrapper扩展了更新语句的set 部份, 可以指定更新的内容
- AbstractLambdaWrapper和AbstractWrapper的功能基本一样, 只是在构建条件时, 要使用Lambda语法
使用 QueryWrapper 构建复杂的查询条件
@SpringBootTest
class UserMapperTest {
@Autowired
private UserMapper userMapper;
/**
* 查询操作:
*/
@Test
void testQueryWrapper() {
/**
* 1.需求:
* 查询名字中带o的, 存款大于等于1000元的人的id, username, info, balance字段
*
* 2.sql:
* select id,username,info,balance
* from user
* where username like ? ans balance ?
*/
// 1.构建查询条件
QueryWrapper<User> wrapper = new QueryWrapper<User>()
.select("id", "username", "info", "balance")
.like("username", "o")
.ge("balance", 1000);
// 2.查询
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}
/**
* 更新操作
*/
@Test
void testUpdateByQueryWrapper() {
/**
* 1.需求:
* 更新用户名为jack的用户的余额为2000
*
* 2.sql:
* 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 构建复杂的更新条件
@SpringBootTest
class UserMapperTest {
@Autowired
private UserMapper userMapper;
/**
* 复杂更新
*/
@Test
void testUpdateWrapper() {
/**
*1.需求:
*更新id为1,2,4的用户余额,扣200
*
*2.sql:
* update user
* set balance = balance - 200
* where id in (1,2,3)
*/
// 1.要更新的数据
List<Long> ids = List.of(1L, 2L, 3L, 4L);
// 3.更新的条件
UpdateWrapper<User> wrapper = new UpdateWrapper<User>()
.setSql("balance = balance - 200")
.in("id", ids);
// 3.执行更新
userMapper.update(null, wrapper);
}
}
使用LamdbaQueryWrapper, 避免字符串魔法值
@SpringBootTest
class UserMapperTest {
@Autowired
private UserMapper userMapper;
/**
* 使用 LamdbaQueryWrapper
*/
@Test
void testLambdaQueryWrapper() {
// 构建查询条件
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>()
.select(User::getId, User::getUsername, User::getInfo, User::getBalance)
.like(User::getUsername, "o")
.ge(User::getBalance, 1000);
// 查询
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}
}
- 传入的不再是字符串, 而是字段的get函数
- MP内部通过反射就能得到操作字段的字段名
- 由于使用了反射机制, 所以代码执行时, 开发工具会爆出警告, 无视就可以了
使用条件构造器的总结
- QueryWrapper通常用来构建select delete update 语句的 where 条件部分
- UpdateWrapper通常只有在 set语句 比较特殊时使用
- 尽量使用LanbdaQueryrapper 和 LambdaUpdateWrapper, 避免硬编码
自定义sql
我们可以利用MyBatisPlus的Wrapper来构建复杂的Where条件, 然后自己定义SQL语句中剩下的部分
使用场景: 更新条件非常繁琐, 更新数据又要动态计算时
- 如果自己编写更新sql, 更新条件要写很多代码
- 如果使用MP的条件构建器自动生成更新sql, 在service层中就会耦合mapper层的代码
- 这时就适合自定义sql, 让MP生成更新条件, 自己定义更新数据
使用步骤
- 基于Wrapper构建where条件
@SpringBootTest
class UserMapperTest {
@Autowired
private UserMapper userMapper;
/**
* 自定义sql
*/
@Test
void testCustomSqlUpdate() {
List<Long> ids = List.of(1L, 2L, 4L);
int amount = 200;
// 构建更新条件
QueryWrapper<User> wrapper = new QueryWrapper<User>().in("id", ids);
// 调用自定义SQL方法
userMapper.updateBalanceByIds(wrapper, amount);
}
}
- 在mapper方法参数中使用 @Param注解 声明wrapper变量名称, 必须是ew
public interface UserMapper extends BaseMapper<User> {
void updateBalanceByIds(@Param(Constants.WRAPPER) QueryWrapper<User> wrapper, @Param("amount") int amount);
}
- 自定义sql, 并使用Wrapper条件
<?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">
<update id="updateBalanceByIds">
update user set balance = balance - #{amount} ${ew.customSqlSegment}
</update>
</mapper>
Service接口
MP给我们提供了Service接口, 只要我们继承了该接口, 基础的增删改查代码就不用自己的开发了
IService接口定义了很多增删改查方法, ServiceImpl实现类实现了IService接口中的所有方法
使用步骤
- 自定义Service接口继承IService接口
public interface IUserService extends IService<User> {
}
- 自定义实现类, 实现自定义接口并继承ServiceImpl类
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
}
- 使用MP提供的增删改查方法
@SpringBootTest
class IUserServiceTest {
@Autowired
private IUserService uerService;
/**
* 测试新增方法
*/
@Test
void testSaveUser() {
User user = new User();
user.setUsername("liuliu");
user.setPassword("123");
user.setPhone("18688990011");
user.setBalance(200);
user.setInfo("{\"age\": 24, \"intro\": \"英文老师\", \"gender\": \"female\"}");
user.setCreateTime(LocalDateTime.now());
user.setUpdateTime(LocalDateTime.now());
uerService.save(user);
}
/**
* 测试查询方法
*/
@Test
void testQuery() {
List<Long> ids = List.of(1L, 2L, 4L);
List<User> users = uerService.listByIds(ids);
users.forEach(System.out::println);
}
}
案例1
基于Restful风格实现下面的接口
- 准备工作
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
... ...
<dependencies>
... ...
// 引入需要的依赖
<!--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>
</dependencies>
... ...
</project>
# 配置swagger
knife4j:
enable: true
openapi:
title: 用户管理接口文档
description: "用户管理接口文档"
email: [email protected]
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
@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;
}
@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;
}
- 实现简单业务接口
@Api(tags = "用户管理接口")
@RequestMapping("/users")
@RestController
@RequiredArgsConstructor
public class UserController {
private final IUserService userService;
@ApiOperation("新增用户接口")
@PostMapping
public void saveUser(@RequestBody UserFormDTO userDTO) {
// 1,把DTO拷贝到PO
// 使用 cn.hutool.core.bean 包下的 BeanUtil 工具类
User user = BeanUtil.copyProperties(userDTO, User.class);
// 2,新增
userService.save(user);
}
@ApiOperation("根据id查询用户接口")
@GetMapping("{id}")
public UserVO queryUserById(@ApiParam("用户id") @PathVariable("id") Long id) {
//1,查询用户PO
User user = userService.getById(id);
//2,把用户PO拷贝到VO
return BeanUtil.copyProperties(user, UserVO.class);
}
@ApiOperation("根据id批量查询用户接口")
@GetMapping
public List<UserVO> queryUserByIds(@ApiParam("用户id集合") @RequestParam("ids") List<Long> ids) {
//1,查询用户PO
List<User> users = userService.listByIds(ids);
//2,把PO拷贝到VO
return BeanUtil.copyToList(users, UserVO.class);
}
@ApiOperation("删除用户接口")
@DeleteMapping("{id}")
public void deleteUserById(@ApiParam("用户id") @PathVariable("id") Long id) {
userService.removeById(id);
}
}
- 以前注入 Service 是使用@Autowired 注解, 这种方式不是Spring推荐的
- Spring推荐使用构造函数注解资源, 但是这种代码太臃肿了
- 使用lombok注解, 来自动生成构造函数, 简化资源的注入
- 实现复杂业务接口
@Api(tags = "用户管理接口")
@RequestMapping("/users")
@RestController
@RequiredArgsConstructor
public class UserController {
private final IUserService userService;
@ApiOperation("扣减用户余额接口")
@PutMapping("/{id}/deduction/{money}")
public void deductBalance(
@ApiParam("用户id") @PathVariable("id") long id,
@ApiParam("扣减的金额") @PathVariable("money") Integer money) {
userService.deductBalance(id, money);
}
}
public interface IUserService extends IService<User> {
void deductBalance(long id, Integer money);
}
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Override
@Transactional
public void deductBalance(long id, Integer money) {
//1,查询用户
User user = getById(id);
//2,校验用户状态
if (user == null || user.getStatus() == UserStatus.FROZEN) {
throw new RuntimeException("用户状态异常!");
}
//3.检查余额是否充足
if (user.getBalance() < money) {
throw new RuntimeException("用户余额不足!");
}
//4,扣减余额
baseMapper.deductBalance(id, money);
}
}
public interface UserMapper extends BaseMapper<User> {
/**
* 根据id扣减余额
*
* @param id
* @param money
*/
@Update("update user set balance = balance- #{money} where id = #{id}")
void deductBalance(long id, Integer money);
}
1. 对于简单的需求, 使用MP提供的增删改查方法就可以, 非常简单
- 对于复杂的业务需求, 我们还是需要自定义Service和自定义Mapper进行代码实现
接口测试
案例2
使用IService的lambdaQuery方法 完成复杂查询
@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;
}
@Api(tags = "用户管理接口")
@RequestMapping("/users")
@RestController
@RequiredArgsConstructor
public class UserController {
private final IUserService userService;
@ApiOperation("根据复杂条件查询用户接口")
@GetMapping("/list")
public List<UserVO> queryUser(UserQuery query) {
//1,查询用户PO
List<User> users = userService.query(query.getName(), query.getStatus(), query.getMinBalance(), query.getMaxBalance());
//2,把PO拷贝到VO
return BeanUtil.copyToList(users,UserVO.class);
}
}
public interface IUserService extends IService<User> {
List<User> query(String name, Integer status, Integer minBalance, Integer maxBalance);
}
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Override
public List<User> query(String name, Integer status, Integer minBalance, Integer maxBalance) {
List<User> userList = lambdaQuery()
.like(name != null, User::getUsername, name)
.eq(status != null, User::getStatus, status)
.ge(minBalance != null, User::getBalance, minBalance)
.le(maxBalance != null, User::getBalance, maxBalance)
.list();
return userList;
}
}
使用IService的lambdaUpdate方法完成复杂更新
@Api(tags = "用户管理接口")
@RequestMapping("/users")
@RestController
@RequiredArgsConstructor
public class UserController {
private final IUserService userService;
@ApiOperation("扣减用户余额接口")
@PutMapping("/{id}/deduction/{money}")
public void deductBalance(
@ApiParam("用户id") @PathVariable("id") long id,
@ApiParam("扣减的金额") @PathVariable("money") Integer money) {
userService.deductBalance(id, money);
}
}
public interface IUserService extends IService<User> {
void deductBalance(long id, Integer money);
}
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Override
@Transactional
public void deductBalance(long id, Integer money) {
//1,查询用户
User user = getById(id);
//2,校验用户状态
if (user == null || user.getStatus() == UserStatus.FROZEN) {
throw new RuntimeException("用户状态异常!");
}
//3.检查余额是否充足
if (user.getBalance() < money) {
throw new RuntimeException("用户余额不足!");
}
//4,扣减余额
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();
}
}
- 并发安全风险: 如果同时有多个线程来做查询, 有可能查到同一个用户, 同一个用户两个线程各自计算余额, 计算的结果是一样的, 就有可能应该扣费2次, 由于并发执行, 最终只扣费1次
- 为了避免并发风险, 我们这里加一个乐观锁, 只有用户余额等于刚才查到的余额, 才执行sql操作, 否则说明用户余额被修改过了, sql就不能执行了
- 添加一个事务注解, 开启事务, 保证sql执行失败后数据的一致性
案例3
需求: 批量插入10万条用户数据, 越快越好
方案1: 普通for循环插入
@SpringBootTest
class IUserServiceTest {
@Autowired
private IUserService uerService;
/**
* 创建数据
*/
private User buildUser(int i) {
User user = new User();
user.setUsername("user_" + i);
user.setPassword("123");
user.setPhone("" + (15553266209L + i));
user.setBalance(2000);
user.setInfo("{\"age\":24,\"intro\":\"英语老师\",\"gender\":\"female\"}");
user.setCreateTime(LocalDateTime.now());
user.setUpdateTime(LocalDateTime.now());
return user;
}
/**
* 测试新增10万数据--普通for循环
*/
@Test
void testSaveOneByOne() {
long b = System.currentTimeMillis();
for (int i = 1; i < 100000; i++) {
uerService.save(buildUser(i));
}
long e = System.currentTimeMillis();
System.out.println("耗时:" + (e - b));
}
}
- 每次请求只提交一条数据, 数据越多网络请求越多
- 每条sql逐条执行, 执行性能也会较差
- 耗时
方案2: ISevice的批量提交
@SpringBootTest
class IUserServiceTest {
@Autowired
private IUserService uerService;
/**
* 创建数据
*/
private User buildUser(int i) {
User user = new User();
user.setUsername("user_" + i);
user.setPassword("123");
user.setPhone("" + (15553266209L + i));
user.setBalance(2000);
user.setInfo("{\"age\":24,\"intro\":\"英语老师\",\"gender\":\"female\"}");
user.setCreateTime(LocalDateTime.now());
user.setUpdateTime(LocalDateTime.now());
return user;
}
/**
* 测试新增10万数据--预编译sql
*/
@Test
void testSaveBatch() {
//思路: 每次插入1000条数据, 插入100次
//1准备容量为1000的集合
List<User> list = new ArrayList<>(1000);
long b = System.currentTimeMillis();
for (int i = 1; i <= 100000; i++) {
//2添加数据到集合
list.add(buildUser(i));
//3集合数据插入到数据库
if (i % 1000 == 0) {
uerService.saveBatch(list);
//4清空集合, 准备下一批数据
list.clear();
}
}
long e = System.currentTimeMillis();
System.out.println("耗时:" + (e - b));
}
}
- 默认情况下的MP批量提交, 采用预编译sql的模式, 一次提交很多数据, 网络请求数量大幅减少
- 批量提交并不是批量新增, 本质还是一条sql插入一条数据, 性能并不是最优
- 先清除数据再测试: delete from user where id > 5;
- 耗时:
方案3: 配置jdbc参数
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: 1234
@SpringBootTest
class IUserServiceTest {
@Autowired
private IUserService uerService;
/**
* 创建数据
*/
private User buildUser(int i) {
User user = new User();
user.setUsername("user_" + i);
user.setPassword("123");
user.setPhone("" + (15553266209L + i));
user.setBalance(2000);
user.setInfo("{\"age\":24,\"intro\":\"英语老师\",\"gender\":\"female\"}");
user.setCreateTime(LocalDateTime.now());
user.setUpdateTime(LocalDateTime.now());
return user;
}
/**
* 测试新增10万数据--预编译sql
*/
@Test
void testSaveBatch() {
//思路: 每次插入1000条数据, 插入100次
//1准备容量为1000的集合
List<User> list = new ArrayList<>(1000);
long b = System.currentTimeMillis();
for (int i = 1; i <= 100000; i++) {
//2添加数据到集合
list.add(buildUser(i));
//3集合数据插入到数据库
if (i % 1000 == 0) {
uerService.saveBatch(list);
//4清空集合, 准备下一批数据
list.clear();
}
}
long e = System.currentTimeMillis();
System.out.println("耗时:" + (e - b));
}
}
- url参数后面手动拼接 &rewriteBatchedStatements=true
- 把一条一条的预编译sql, 重写为1条sql, 数据批量插入性能最好
- 先清除数据再测试: delete from user where id > 5;
- 耗时