目录
一:前言
MyBatis Plus是一款基于MyBatis的轻量级ORM框架,简化了MyBatis的开发流程,提供了更加便捷的CRUD操作。以下是MyBatis Plus的一些特点和功能:
代码生成器:MyBatis Plus提供了代码生成器,可以根据数据库表结构自动生成实体类、Mapper接口和XML文件,减少了手动编写代码的工作量。
强大的CRUD操作:MyBatis Plus提供了很多常用的CRUD操作方法,如插入、更新、删除、查询等,可以通过编写简单的方法实现复杂的数据库操作。
条件构造器:MyBatis Plus提供了灵活的条件构造器,可以通过链式调用的方式构建查询条件,支持动态条件拼接和条件组合,简化了复杂查询的编写。
分页查询:MyBatis Plus提供了分页查询的支持,可以通过简单的方法调用实现分页查询,并且支持自动计算总记录数。
逻辑删除:MyBatis Plus支持逻辑删除功能,可以通过设置字段值为特定的标识值进行逻辑删除,同时还提供了逻辑删除的查询方法。
自动填充:MyBatis Plus支持自动填充功能,可以在插入和更新操作时自动填充指定的字段值,如创建时间和更新时间等。
乐观锁支持:MyBatis Plus提供了乐观锁支持,可以通过在实体类中添加版本号字段来实现乐观锁功能,避免数据冲突。
总的来说,MyBatis Plus是一个功能强大、易于使用的ORM框架,可以大大提高开发效率,减少重复的工作,同时还提供了一些方便的功能来简化数据库操作。
二.快速入门
为了方便测试,我们先创建一个新的项目,并准备一些基础数据。
1.1.环境准备
首先我们需要准备好一个一个项目到自己的工作空间(不要包含空格和特殊字符).
然后用你的IDEA工具打开,项目结构如下:
接下来,要导入两张表,分别是user和address表:
表内数据如下图:
user表:
address表:
2.2.快速开始
比如我们要实现User表的CRUD,只需要下面几步:
-
引入MybatisPlus依赖
-
定义Mapper
2.2.1引入依赖
MybatisPlus提供了starter,实现了自动Mybatis以及MybatisPlus的自动装配功能,坐标如下:
<!-- mybatis-plus依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
由于这个starter包含对mybatis的自动装配,因此完全可以替换掉Mybatis的starter。 最终,项目的依赖如下:
<dependencies>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
2.2.2.定义Mapper
为了简化单表CRUD,MybatisPlus提供了一个基础的BaseMapper
接口,其中已经实现了单表的CRUD:
因此我们自定义的Mapper只要实现了这个BaseMapper
,就无需自己实现单表CRUD了。
2.2.3.测试
新建一个测试类,编写几个单元测试,测试基本的CRUD功能:
package com.itheima.mp.mapper;
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.List;
@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("{\"age\": 24, \"intro\": \"英文老师\", \"gender\": \"female\"}");
user.setCreateTime(LocalDateTime.now());
user.setUpdateTime(LocalDateTime.now());
userMapper.insert(user);
}
@Test
void testSelectById() {
User user = userMapper.selectById(5L);
System.out.println("user = " + user);
}
@Test
void testSelectByIds() {
List<User> users = userMapper.selectBatchIds(List.of(1L, 2L, 3L, 4L, 5L));
users.forEach(System.out::println);
}
@Test
void testUpdateById() {
User user = new User();
user.setId(5L);
user.setBalance(20000);
userMapper.updateById(user);
}
@Test
void testDelete() {
userMapper.deleteById(5L);
}
}
例如我们通过执行根据id查询用户功能,可以看到,在运行过程中打印出的SQL日志,非常标准:
只需要继承BaseMapper就能省去所有的单表CRUD,是不是非常简单!
2.3.常见注解
在刚刚的入门案例中,我们仅仅引入了依赖,继承了BaseMapper就能使用MybatisPlus,非常简单。但是问题来了: MybatisPlus如何知道我们要查询的是哪张表?表中有哪些字段呢?
大家回忆一下,UserMapper在继承BaseMapper的时候指定了一个泛型:
public interface UserMapper extends BaseMapper<User>
泛型中的User就是与数据库对应的PO.
MybatisPlus就是根据PO实体的信息来推断出表的信息,从而生成SQL的。默认情况下:
-
MybatisPlus会把PO实体的类名驼峰转下划线作为表名
-
MybatisPlus会把PO实体的所有变量名驼峰转下划线作为表的字段名,并根据变量类型推断字段类型
-
MybatisPlus会把名为id的字段作为主键
但很多情况下,默认的实现与实际场景不符,因此MybatisPlus提供了一些注解便于我们声明表信息。
2.3.1.@TableName
说明:
描述:表名注解,标识实体类对应的表
使用位置:实体类
@TableName("user")
public class User {
private Long id;
private String name;
}
TableName注解除了指定表名以外,还可以指定很多其它属性:
属性 | 类型 | 必须指定 | 默认值 | 描述 |
value | String | 否 | "" | 表名 |
schema | String | 否 | "" | schema |
keepGlobalPrefix | boolean | 否 | false | 是否保持使用全局的 tablePrefix 的值(当全局 tablePrefix 生效时) |
resultMap | String | 否 | "" | xml 中 resultMap 的 id(用于满足特定类型的实体类对象绑定) |
autoResultMap | boolean | 否 | false | 是否自动构建 resultMap 并使用(如果设置 resultMap 则不会进行 resultMap 的自动构建与注入) |
excludeProperty | String[] | 否 | {} | 需要排除的属性名 @since 3.3.1 |
2.3.2.@TableId
说明:
描述:主键注解,标识实体类中的主键字段
使用位置:实体类的主键字段
示例:
@TableName("user") public class User { @TableId private Long id; private String name; }
IdType
支持的类型有:
值 | 描述 |
---|---|
AUTO | 数据库 ID 自增 |
NONE | 无状态,该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT) |
INPUT | insert 前自行 set 主键值 |
ASSIGN_ID | 分配 ID(主键类型为 Number(Long 和 Integer)或 String)(since 3.3.0),使用接口IdentifierGenerator的方法nextId(默认实现类为DefaultIdentifierGenerator雪花算法) |
ASSIGN_UUID | 分配 UUID,主键类型为 String(since 3.3.0),使用接口IdentifierGenerator的方法nextUUID(默认 default 方法) |
| 分布式全局唯一 ID 长整型类型(please use ASSIGN_ID) |
| 32 位 UUID 字符串(please use ASSIGN_UUID) |
| 分布式全局唯一 ID 字符串类型(please use ASSIGN_ID) |
这里比较常见的有三种:
-
AUTO
:利用数据库的id自增长 -
INPUT
:手动生成id -
ASSIGN_ID
:雪花算法生成Long
类型的全局唯一id,这是默认的ID策略
2.3.3.@TableField
说明:
描述:普通字段注解
示例:
@TableName("user")
public class User {
@TableId
private Long id;
private String name;
private Integer age;
@TableField("isMarried")
private Boolean isMarried;
@TableField("concat")
private String concat;
}
一般情况下我们并不需要给字段添加@TableField
注解,一些特殊情况除外:
-
成员变量名与数据库字段名不一致
-
成员变量是以
isXXX
命名,按照JavaBean
的规范,MybatisPlus
识别字段时会把is
去除,这就导致与数据库不符。 -
成员变量名与数据库一致,但是与数据库的关键字冲突。使用
@TableField
注解给字段名添加转义字符:``
支持的其它属性如下:
属性 | 类型 | 必填 | 默认值 | 描述 |
value | String | 否 | "" | 数据库字段名 |
exist | boolean | 否 | true | 是否为数据库表字段 |
condition | String | 否 | "" | 字段 where 实体查询比较条件,有值设置则按设置的值为准,没有则为默认全局的 %s=#{%s},参考(opens new window) |
update | String | 否 | "" | 字段 update set 部分注入,例如:当在version字段上注解update="%s+1" 表示更新时会 set version=version+1 (该属性优先级高于 el 属性) |
insertStrategy | Enum | 否 | FieldStrategy.DEFAULT | 举例:NOT_NULL insert into table_a(<if test="columnProperty != null">column</if>) values (<if test="columnProperty != null">#{columnProperty}</if>) |
updateStrategy | Enum | 否 | FieldStrategy.DEFAULT | 举例:IGNORED update table_a set column=#{columnProperty} |
whereStrategy | Enum | 否 | FieldStrategy.DEFAULT | 举例:NOT_EMPTY where <if test="columnProperty != null and columnProperty!=''">column=#{columnProperty}</if> |
fill | Enum | 否 | FieldFill.DEFAULT | 字段自动填充策略 |
select | boolean | 否 | true | 是否进行 select 查询 |
keepGlobalFormat | boolean | 否 | false | 是否保持使用全局的 format 进行处理 |
jdbcType | JdbcType | 否 | JdbcType.UNDEFINED | JDBC 类型 (该默认值不代表会按照该值生效) |
typeHandler | TypeHander | 否 | 类型处理器 (该默认值不代表会按照该值生效) | |
numericScale | String | 否 | "" | 指定小数点后保留的位数 |
2.4.常见配置
MybatisPlus也支持基于yaml文件的自定义配置,详见官方文档:
使用配置 | mybatis-plushttps://www.baomidou.com/pages/56bac0/#%E5%9F%BA%E6%9C%AC%E9%85%8D%E7%BD%AE
大多数的配置都有默认值,因此我们都无需配置。但还有一些是没有默认值的,例如:
-
实体类的别名扫描包
-
全局id类型
mybatis-plus:
type-aliases-package: com.itheima.mp.domain.po
global-config:
db-config:
id-type: auto # 全局id类型为自增长
三. 核心功能
刚才的案例中都是以id为条件的简单CRUD,一些复杂条件的SQL语句就要用到一些更高级的功能了。
3.1.条件构造器
除了新增以外,修改、删除、查询的SQL语句都需要指定where条件。因此BaseMapper中提供的相关方法除了以id
作为where
条件以外,还支持更加复杂的where
条件。
参数中的Wrapper
就是条件构造的抽象类,其下有很多默认实现,继承关系如图:
接下来,我们就来看看如何利用Wrapper
实现复杂查询。
3.1.1.QueryWrapper
无论是修改、删除、查询,都可以使用QueryWrapper来构建查询条件。接下来看一些例子: 查询:查询出名字中带o
的,存款大于等于1000元的人。代码如下:
@Test
void testUserByQueryWrapper() {
//查询出名字中带o的,存款大于等于1000元的人
QueryWrapper<User> wrapper = new QueryWrapper<User>()
.like("userName", "o")
.le("balance", 1000);
List<User> users = userMapper.selectList(wrapper);
System.out.println("users = " + users);
}
运行结果:
3.1.2.UpdateWrapper
基于BaseMapper中的update方法更新时只能直接赋值,对于一些复杂的需求就难以实现。 例如:更新id为1,2,4
的用户的余额,扣200,对应的SQL应该是:
@Test
void testUpdateWrapper() {
//更新id为1,2,4的用户的余额,扣200
UpdateWrapper<User> wrapper = new UpdateWrapper<User>()
.setSql("balance = balance - 200")
.in("id", 1L, 2L, 4L);
userMapper.update(null, wrapper);
}
2.1.3.LambdaQueryWrapper
无论是QueryWrapper还是UpdateWrapper在构造条件的时候都需要写死字段名称,会出现字符串
魔法值
。这在编程规范中显然是不推荐的。 那怎么样才能不写字段名,又能知道字段名呢?其中一种办法是基于变量的
getter
方法结合反射技术。因此我们只要将条件对应的字段的getter
方法传递给MybatisPlus,它就能计算出对应的变量名了。而传递方法可以使用JDK8中的方法引用
和Lambda
表达式。 因此MybatisPlus又提供了一套基于Lambda的Wrapper,包含两个:
LambdaQueryWrapper
LambdaUpdateWrapper
分别对应QueryWrapper和UpdateWrapper
其使用方式如下:
@Test
void testLambdaQueryWrapper() {
//查询出名字中带o的,存款大于等于1000元的人
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>()
.like(User::getUsername, "o")
.ge(User::getBalance, 1000);
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}
3.2.自定义SQL
在演示UpdateWrapper的案例中,我们在代码中编写了更新的SQL语句:
这种写法在某些企业也是不允许的,因为SQL语句最好都维护在持久层,而不是业务层。就当前案例来说,由于条件是in语句,只能将SQL写在Mapper.xml文件,利用foreach来生成动态SQL。 这实在是太麻烦了。假如查询条件更复杂,动态SQL的编写也会更加复杂。
所以,MybatisPlus提供了自定义SQL功能,可以让我们利用Wrapper生成查询条件,再结合Mapper.xml编写SQL。
3.2.1.基本用法
以当前案例来说,我们可以这样写:
/**
* mybatis-plus自定义sql
*/
@Test
void testCustomUpdateWrapper() {
//前端传的参数
List<Long> ids = List.of(1L, 2L, 3L);
int amount = 200;
//构建where条件
QueryWrapper<User> wrapper = new QueryWrapper<User>()
.in("id", ids);
//执行更新语句
userMapper.updateByIds(wrapper, amount);
}
然后在UserMapper.xml中自定义SQL:
<?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="updateByIds">
update user
set balance=balance - #{amount} ${ew.customSqlSegment}
</update>
</mapper>
这样就省去了编写复杂查询条件的烦恼了。
3.2.2.多表关联
理论上来讲MyBatisPlus是不支持多表查询的,不过我们可以利用Wrapper中自定义条件结合自定义SQL来实现多表查询的效果。 例如,我们要查询出所有收货地址在北京的并且用户id在1、2、4之中的用户 要是自己基于mybatis实现SQL,大概是这样的:
<select id="queryUserByIdAndAddr" resultType="com.itheima.mp.domain.po.User">
SELECT *
FROM user u
INNER JOIN address a ON u.id = a.user_id
WHERE u.id
<foreach collection="ids" separator="," item="id" open="IN (" close=")">
#{id}
</foreach>
AND a.city = #{city}
</select>
可以看出其中最复杂的就是WHERE条件的编写,如果业务复杂一些,这里的SQL会更变态。
但是基于自定义SQL结合Wrapper的玩法,我们就可以利用Wrapper来构建查询条件,然后手写SELECT及FROM部分,实现多表查询。
查询条件这样来构建:
@Test
void testCustomJoinWrapper() {
//构建where条件
QueryWrapper<User> wrapper = new QueryWrapper<User>()
.in("u.id", List.of(1L, 2L, 4L))
.eq("a.city", "北京");
List<User> users = userMapper.queryUserByWrapper(wrapper);
users.forEach(System.out::println);
}
然后在UserMapper.xml中自定义SQL:
<select id="queryUserByWrapper" resultType="com.itheima.mp.domain.po.User">
select * from user u inner join address a on u.id=a.user_id ${ew.customSqlSegment}
</select>
3.3.Service接口
MybatisPlus不仅提供了BaseMapper,还提供了通用的Service接口及默认实现,封装了一些常用的service模板方法。 通用接口为IService
,默认实现为ServiceImpl
,其中封装的方法可以分为以下几类:
-
save
:新增 -
remove
:删除 -
update
:更新 -
get
:查询单个结果 -
list
:查询集合结果 -
count
:计数 -
page
:分页查询
3.3.1.CRUD
我们先俩看下基本的CRUD接口。
新增:
-
save
是新增单个元素 -
saveBatch
是批量新增 -
saveOrUpdate
是根据id判断,如果数据存在就更新,不存在则新增 -
saveOrUpdateBatch
是批量的新增或修改
删除:
-
removeById
:根据id删除 -
removeByIds
:根据id批量删除 -
removeByMap
:根据Map中的键值对为条件删除 -
remove(Wrapper<T>)
:根据Wrapper条件删除
修改:
-
updateById
:根据id修改 -
update(Wrapper<T>)
:根据UpdateWrapper
修改,Wrapper
中包含set
和where
部分 -
update(T,Wrapper<T>)
:按照T
内的数据修改与Wrapper
匹配到的数据 -
updateBatchById
:根据id批量修改
Get:
-
getById
:根据id查询1条数据 -
getOne(Wrapper<T>)
:根据Wrapper
查询1条数据 -
getBaseMapper
:获取Service
内的BaseMapper
实现,某些时候需要直接调用Mapper
内的自定义SQL
时可以用这个方法获取到Mapper
List:
-
listByIds
:根据id批量查询 -
list(Wrapper<T>)
:根据Wrapper条件查询多条数据 -
list()
:查询所有
Count:
-
count()
:统计所有数量 -
count(Wrapper<T>)
:统计符合Wrapper
条件的数据数量
getBaseMapper: 当我们在service中要调用Mapper中自定义SQL时,就必须获取service对应的Mapper,就可以通过这个方法:
3.3.2.基本用法
由于Service
中经常需要定义与业务有关的自定义方法,因此我们不能直接使用IService
,而是自定义Service
接口,然后继承IService
以拓展方法。同时,让自定义的Service实现类
继承ServiceImpl
,这样就不用自己实现IService
中的接口了。
首先,定义IUserService
,继承IService
:
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> {
// 拓展自定义方法
}
然后,编写UserServiceImpl
类,继承ServiceImpl
,实现UserService
:
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.domain.po.service.IUserService;
import com.itheima.mp.mapper.UserMapper;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
}
接下来,我们快速实现下面4个接口:
编号 | 接口 | 请求方式 | 请求路径 | 请求参数 |
---|---|---|---|---|
1 | 新增用户 | POST | /users | 用户表单实体 |
2 | 删除用户 | DELETE | /users/{id} | 用户id |
3 | 根据id查询用户 | GET | /users/{id} | 用户id |
4 | 根据id批量查询 | GET | /users | 用户id集合 |
首先,我们在项目中引入几个依赖:
<!--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>
通过配置接口文档用于测试:
knife4j:
enable: true
openapi:
title: 用户管理接口文档
description: "用户管理接口文档"
email: zhanghuyi@itcast.cn
concat: hhhh
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:代表新增时的用户表单
-
UserVO:代表查询的返回结果
首先是UserFormDTO:
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:
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;
}
最后,按照Restful风格编写Controller接口方法:
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
@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);
}
}
3.3.3.Lambda
IService中还提供了Lambda功能来简化我们的复杂查询及更新功能。我们通过两个案例来学习一下。
案例一:实现一个根据复杂条件查询用户的接口,查询条件如下:
-
name:用户名关键字,可以为空
-
status:用户状态,可以为空
-
minBalance:最小余额,可以为空
-
maxBalance:最大余额,可以为空
可以理解成一个用户的后台管理界面,管理员可以自己选择条件来筛选用户,因此上述条件不一定存在,需要做判断。
我们首先需要定义一个查询条件实体,UserQuery实体:
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方法:
@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
方法:
@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部分是动态的。
实现如下:
@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();
}
四.扩展功能
4.1.代码生成
在使用MybatisPlus以后,基础的Mapper
、Service
、PO
代码相对固定,重复编写也比较麻烦。因此MybatisPlus官方提供了代码生成器根据数据库表结构生成PO
、Mapper
、Service
等相关代码。只不过代码生成器同样要编码使用,也很麻烦。
这里推荐大家使用一款MybatisPlus
的插件,它可以基于图形化界面完成MybatisPlus
的代码生成,非常简单。
4.1.1.安装插件
在Idea
的plugins市场中搜索并安装MyBatisPlus
插件:
4.1.2.使用
刚好数据库中还有一张address表尚未生成对应的实体和mapper等基础代码。我们利用插件生成一下。 首先需要配置数据库地址,在Idea顶部菜单中,找到other
,选择
在弹出的窗口中填写数据库连接的基本信息:
点击OK保存。
然后再次点击Idea顶部菜单中的other,然后选择Code Generator
:
最终,代码自动生成到指定的位置了。
4.2.静态工具
有的时候Service之间也会相互调用,为了避免出现循环依赖问题,MybatisPlus提供一个静态工具类:Db
,其中的一些静态方法与IService
中方法签名基本一致,也可以帮助我们实现CRUD功能:
示例:
@Test
void testDbGet() {
User user = Db.getById(1L, User.class);
System.out.println(user);
}
@Test
void testDbList() {
// 利用Db实现复杂条件查询
List<User> list = Db.lambdaQuery(User.class)
.like(User::getUsername, "o")
.ge(User::getBalance, 1000)
.list();
list.forEach(System.out::println);
}
@Test
void testDbUpdate() {
Db.lambdaUpdate(User.class)
.set(User::getBalance, 2000)
.eq(User::getUsername, "Rose");
}
五.插件功能
MybatisPlus提供了很多的插件功能,进一步拓展其功能。目前已有的插件有:
-
PaginationInnerInterceptor
:自动分页 -
TenantLineInnerInterceptor
:多租户 -
DynamicTableNameInnerInterceptor
:动态表名 -
OptimisticLockerInnerInterceptor
:乐观锁 -
IllegalSQLInnerInterceptor
:sql 性能规范 -
BlockAttackInnerInterceptor
:防止全表更新与删除
注意: 使用多个分页插件的时候需要注意插件定义顺序,建议使用顺序如下:
多租户,动态表名
分页,乐观锁
sql 性能规范,防止全表更新与删除
4.1.分页插件
在未引入分页插件的情况下,MybatisPlus
是不支持分页功能的,IService
和BaseMapper
中的分页方法都无法正常起效。 所以,我们必须配置分页插件。
4.1.1.配置分页插件
在项目中新建一个配置类:
package com.itheima.mp.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
// 初始化核心插件
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
4.1.2.分页API
编写一个分页查询的测试:
@Test
void testPageQuery() {
int pageNo = 1, pageSize = 2;
//1.分页参数
Page<User> page = Page.of(pageNo, pageSize);
//1.1添加排序条件
page.addOrder(new OrderItem("balance", true));
page.addOrder(new OrderItem("id", true));
//
Page<User> p = userService.page(page);
long total = p.getTotal();//获取总条数
System.out.println("total = " + total);
long pages = p.getPages();//获取总页数
System.out.println("pages = " + pages);
List<User> records = page.getRecords();//获取数据
System.out.println("records = " + records);
}
j运行结果如下:
这里用到了分页参数,Page,即可以支持分页参数,也可以支持排序参数。常见的API如下:
int pageNo = 1, pageSize = 5;
// 分页参数
Page<User> page = Page.of(pageNo, pageSize);
// 排序参数, 通过OrderItem来指定
page.addOrder(new OrderItem("balance", false));
userService.page(page);
4.2.通用分页实体
现在要实现一个用户分页查询的接口,接口规范如下:
参数 | 说明 |
请求方式 | GET |
请求路径 | /users/page |
请求参数 |
|
返回值 |
|
特殊说明 |
|
这里需要定义3个实体:
-
UserQuery
:分页查询条件的实体,包含分页、排序参数、过滤条件 -
PageDTO
:分页结果实体,包含总条数、总页数、当前页数据 -
UserVO
:用户页面视图实体
4.2.2.开发接口
我们在UserController
中定义分页查询用户的接口:
@ApiOperation("条件分页查询")
@GetMapping("/page")
public PageDTO<UserVO> queryUsersPage(UserQuery query) {
return userService.queryUsersPage(query);
}
然后在IUserService
中创建queryUsersPage
方法:
PageDTO<UserVO> queryUsersPage(UserQuery userQuery);
接下来,在UserServiceImpl中实现该方法:
@Override
public PageDTO<UserVO> queryUsersPage(UserQuery query) {
// 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(), true));
} else {
//1.3 按照跟新时间降序排序
page.addOrder(new OrderItem("update_time", false));
}
Page<User> p = lambdaQuery()
.eq(query.getName() != null, User::getUsername, query.getName())
.eq(query.getStatus() != null, User::getStatus, query.getStatus())
.ge(query.getMinBalance() != null, User::getBalance, query.getMinBalance())
.le(query.getMaxBalance() != null, User::getBalance, query.getMaxBalance())
.page(page);
//获取用户
List<User> users = p.getRecords();
//获取用户id集合
List<Long> userIds = users.stream().map(User::getId).collect(Collectors.toList());
//根据用户id查询地址
List<Address> addresses = Db.lambdaQuery(Address.class).in(Address::getUserId, userIds).list();
Map<Long, List<Address>> addressMap = new HashMap<>();
if (CollUtil.isNotEmpty(addresses)) {
// 用户地址分组处理
addressMap = addresses.stream().collect(Collectors.groupingBy(Address::getUserId));
}
//创建userVo集合
List<UserVO> userVOS = new ArrayList<>();
for (User user : users) {
UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
userVO.setAddresses(BeanUtil.copyToList(addressMap.get(user.getId()), AddressVO.class));
userVOS.add(userVO);
}
//封装pageDto结果并返回
PageDTO<UserVO> pageDTO = new PageDTO<>();
pageDTO.setPages(p.getPages());
pageDTO.setTotal(p.getTotal());
pageDTO.setList(userVOS);
//封装vo结果返回
return pageDTO;
}
通过接口文档测试结果: