2.核心功能
刚才的案例中都是以id
为条件的简单CRUD
,一些复杂条件的SQL
语句就要用到一些更高级的功能了。
2.1.条件构造器
除了新增以外,修改、删除、查询的SQL
语句都需要指定where
条件。因此BaseMapper
中提供的相关方法除了以id
作为where
条件以外,还支持更加复杂的where
条件。
参数中的Wrapper
就是条件构造的抽象类,其下有很多默认实现,继承关系如图:
Wrapper
的子类AbstractWrapper
提供了where
中包含的所有条件构造方法:
而QueryWrapper
在AbstractWrapper
的基础上拓展了一个select
方法,允许指定查询字段:
而UpdateWrapper
在AbstractWrapper
的基础上拓展了一个set
方法,允许指定SQL
中的SET
部分:
2.1.1.QueryWrapper
无论是修改、删除、查询,都可以使用QueryWrapper
来构建查询条件。接下来看一些例子:
1.查询:查询出名字中带o
的,存款大于等于1000
元的人。代码如下:
@Test
void testQueryWrapper() {// 1.构建查询条件 where name like "%o%" AND balance >= 1000QueryWrapper<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);
}
更新:更新用户名为Jack
的用户的余额为2000
,代码如下:
@Test
void testUpdateByQueryWrapper() {// 1.构建查询条件 where name = "Jack"QueryWrapper<User> wrapper = new QueryWrapper<User>().eq("username", "Jack");// 2.更新数据,user中非null字段都会作为set语句User user = new User();user.setBalance(2000);userMapper.update(user, wrapper);
}
2.1.2.UpdateWrapper
基于BaseMapper
中的update
方法更新时只能直接赋值,对于一些复杂的需求就难以实现。
例如:更新id
为1,2,4
的用户的余额,扣200
,对应的SQL
应该是:
UPDATE user SET balance = balance - 200 WHERE id in (1, 2, 4)
SET
的赋值结果是基于字段现有值的,这个时候就要利用UpdateWrapper
中的setSql
功能了:
// 更新id为1,2,4的用户的余额,扣200@Testvoid testUpdateWrapper() {// List<Long> ids = new ArrayList<>(Arrays.asList(1L, 2L, 4L)); //JDK8 可以使用这种方式List<Long> ids = List.of(1L, 2L, 4L); // JDK9 之后才有的of方法// 1.生成SQLUpdateWrapper<User> userUpdateWrapper = new UpdateWrapper<User>().setSql("balance = balance -200").in("id", ids); // WHERE id in (1, 2, 4)// 2.更新,注意第一个参数可以给null,也就是不填更新字段和数据,// 而是基于UpdateWrapper中的setSql来更新userMapper.update(null, userUpdateWrapper);}
2.1.3.LambdaQueryWrapper
无论是QueryWrapper
还是UpdateWrapper
在构造条件的时候都需要写死字段名称,会出现字符串魔法值
。这在编程规范中显然是不推荐的。
那怎么样才能不写字段名,又能知道字段名呢?
其中一种办法是基于变量的gettter
方法结合反射技术。因此我们只要将条件对应的字段的getter
方法传递给MybatisPlus
,它就能计算出对应的变量名了。而传递方法可以使用JDK8
中的方法引用和Lambda
表达式。
因此MybatisPlus
又提供了一套基于Lambda的Wrapper
,包含两个:
-LambdaQueryWrapper
LambdaUpdateWrapper
分别对应QueryWrapper
和UpdateWrapper
其使用方式如下:
@Testvoid testLambdaQueryWrapper() {// 1.构建查询条件 where name like "%o%" AND balance >= 1000LambdaQueryWrapper<User> userLambdaQueryWrapper = new LambdaQueryWrapper<User>().select(User::getId, User::getUsername, User::getInfo, User::getBalance).like(User::getUsername, "o").ge(User::getBalance, 1000);// 2.查询数据List<User> users = userMapper.selectList(userLambdaQueryWrapper);users.forEach(System.out::println);}// 更新用户名为jack的用户的余额为2000@Testvoid testLambdaUpdateByQueryWrapper() {// 1.构建查询条件 where name = "Jack"LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>().eq(User::getUsername, "Jack");// 2.更新数据,user中非null字段都会作为set语句User user = new User();user.setBalance(2000);userMapper.update(user, wrapper);}
2.2.自定义SQL
在演示UpdateWrapper
的案例中,我们在代码中编写了更新的SQL
语句:
这种写法在某些企业也是不允许的,因为SQL
语句最好都维护在持久层,而不是业务层。就当前案例来说,由于条件是in
语句,只能将SQL
写在Mapper.xml
文件,利用foreach
来生成动态SQL
。
这实在是太麻烦了。假如查询条件更复杂,动态SQL
的编写也会更加复杂。
所以,MybatisPlus
提供了自定义SQL
功能,可以让我们利用Wrapper
生成查询条件,再结合Mapper.xml
编写SQL
2.2.1.基本用法
以当前案例来说,我们可以这样写:
@Test
void testCustomWrapper() {// 1.准备自定义查询条件List<Long> ids = List.of(1L, 2L, 4L);QueryWrapper<User> wrapper = new QueryWrapper<User>().in("id", ids);// 2.调用mapper的自定义方法,直接传递WrapperuserMapper.deductBalanceByIds(200, wrapper);
}
然后在UserMapper
中自定义SQL
:
package com.itheima.mp.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.mp.domain.po.User;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
import org.apache.ibatis.annotations.Param;public interface UserMapper extends BaseMapper<User> {@Select("UPDATE user SET balance = balance - #{money} ${ew.customSqlSegment}")void deductBalanceByIds(@Param("money") int money, @Param("ew") QueryWrapper<User> wrapper);
}
这样就省去了编写复杂查询条件的烦恼了。
总结
在
mapper
方法参数中用Param
注解声明wrapper
变量名称,必须是ew
。
为什么必须要用ew
?mybatisPlus
源码规定的,这也成为一种约定俗成的规范。当然,你也可以手动修改生成的代码,将ew
改为其他名字,但这样可能导致一些示例代码不适用。
2.2.2.多表关联
理论上来讲MyBatisPlus
是不支持多表查询的,不过我们可以利用Wrapper
中自定义条件结合自定义SQL
来实现多表查询的效果。
例如,我们要查询出所有收货地址在北京的并且用户id
在1、2、4
之中的用户
要是自己基于mybatis
实现SQL
,大概是这样的:
<select id="queryUserByIdAndAddr" resultType="com.itheima.mp.domain.po.User">SELECT *FROM user uINNER JOIN address a ON u.id = a.user_idWHERE 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() {// 1.准备自定义查询条件QueryWrapper<User> wrapper = new QueryWrapper<User>().in("u.id", List.of(1L, 2L, 4L)).eq("a.city", "北京");// 2.调用mapper的自定义方法List<User> users = userMapper.queryUserByWrapper(wrapper);users.forEach(System.out::println);
}
然后在UserMapper
中自定义方法:
@Select("SELECT u.* FROM user u INNER JOIN address a ON u.id = a.user_id ${ew.customSqlSegment}")
List<User> queryUserByWrapper(@Param("ew")QueryWrapper<User> wrapper);
当然,也可以在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>
2.3.Service接口
MybatisPlus
不仅提供了BaseMapper
,还提供了通用的Service
接口及默认实现,封装了一些常用的service
模板方法。
通用接口为IService
,默认实现为ServiceImpl
,其中封装的方法可以分为以下几类:
save
:新增remove
:删除update
:更新get
:查询单个结果list
:查询集合结果count
:计数page
:分页查询
2.3.1.CRUD
我们先俩看下基本的CRUD
接口。
新增:
save
是新增单个元素saveBatch
是批量新增saveOrUpdate
是根据id
判断,如果数据存在就更新,不存在则新增saveOrUpdateBatch
是批量的新增或修改
删除:
removeById
:根据id
删除removeByIds
:根据id
批量删除removeByMap
:根据Map
中的键值对为条件删除remove(Wrapper<T>)
:根据Wrapper
条件删除:暂不支持removeBatchByIds
修改:
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
,就可以通过这个方法:
项目结构如下:
来到 UserService
中创建 测试类
接下来,我们快速实现下面4个接口:
首先,我们在项目中引入几个依赖:
pom.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>
然后在application.xml
需要配置swagger
信息:
knife4j:enable: trueopenapi:title: 用户管理接口文档description: "用户管理接口文档"email: zhanghuyi@itcast.cnconcat: 墨苒孤url: https://www.itcast.cnversion: v1.0.0group:default:group-name: defaultapi-rule: packageapi-rule-resources:- com.itheima.mp.controller
Alt + 8
选择 springboot
服务,启动,浏览器输入 http://localhost:8080/doc.html#/home
然后,接口需要两个实体:
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;
}
用 @Autowired
注入,Spring
并不推荐,而是推荐我们使用构造函数注入
构造函数注入如下:
但是这样会带来一个问题,如果成员很多,构造函数就会很多,看起来很繁琐。那么就可以使用Lombok
注解帮我们简化
上图这样看起来是可以了,但是,一个类可以有很多的成员,并不是每一个都是需要注入的。那么怎么区分哪些需要注入,哪些不需要注入呢?
可以给 成员 加上 final
来区分, 这样就必须在类初始化的时候,对加了 final
的成员进行初始化,所以最终写法如下:
@RequiredArgsConstructor
表示 会对 final
修饰的成员进行创建构造函数,没有修饰就不会生成构造函数
新增接口实现:
@PostMapping@ApiOperation("新增用户")public void saveUser(@RequestBody UserFormDTO userFormDTO) {// 1.转换DTO为POUser user = BeanUtil.copyProperties(userFormDTO, User.class);// 2.新增userService.save(user);}
由于传入的 是 DTO
, 而我们需要保存的是 PO
,所以需要先把DTO
转换为PO
。这里使用胡图工具包,其余接口同理
@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.处理po转voreturn 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.处理po转vo (集合用copyToList)return BeanUtil.copyToList(users, UserVO.class);}
代码在github
https://github.com/RanGuMo/mp-demo