3.扩展功能
3.1.代码生成
在使用MybatisPlus
以后,基础的Mapper
、Service
、PO
代码相对固定,重复编写也比较麻烦。因此MybatisPlus
官方提供了代码生成器根据数据库表结构生成PO
、Mapper
、Service
等相关代码。只不过代码生成器同样要编码使用,也很麻烦。(也就是说,我要生成代码,还要编写一套代码,让这套代码去生成代码)
这里推荐大家使用一款MybatisPlus
的插件,它可以基于图形化界面完成MybatisPlus
的代码生成,非常简单。
3.1.1.安装插件
在Idea
的setting
中的plugins
市场中搜索并安装MyBatisPlus
插件:
然后重启Idea
即可使用。
3.1.2.使用
刚好数据库中还有一张address
表尚未生成对应的实体和mapper
等基础代码。我们利用插件生成一下。
首先需要配置数据库地址,在Idea
顶部菜单中,找到other
,选择Config Database
:
在弹出的窗口中填写数据库连接的基本信息:
点击OK
保存。
然后再次点击Idea
顶部菜单中的other
,然后选择Code Generator
:
在弹出的表单中填写信息:
最终,代码自动生成到指定的位置。
3.2.静态工具
有的时候Service
之间也会相互调用,为了避免出现循环依赖问题,MybatisPlus
提供一个静态工具类:Db
,其中的一些静态方法与IService
中方法签名基本一致,也可以帮助我们实现CRUD
功能:
由于Db
是静态的,无法通过泛型传递,只能通过传递字节码,得到类名
示例:
@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");
}
案例一:改造根据
id
用户查询的接口,查询用户的同时返回用户收货地址列表
首先,我们要添加一个收货地址的VO
对象:
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 AddressVO{
@ApiModelProperty("id")
private Long id;
@ApiModelProperty("用户ID")
private Long userId;
@ApiModelProperty("省")
private String province;
@ApiModelProperty("市")
private String city;
@ApiModelProperty("县/区")
private String town;
@ApiModelProperty("手机")
private String mobile;
@ApiModelProperty("详细地址")
private String street;
@ApiModelProperty("联系人")
private String contact;
@ApiModelProperty("是否是默认 1默认 0否")
private Boolean isDefault;
@ApiModelProperty("备注")
private String notes;
}
然后,改造原来的UserVO
,添加一个地址属性:
接下来,修改UserController
中根据id
查询用户的业务接口:
@GetMapping("/{id}")
@ApiOperation("根据id查询用户")
public UserVO queryUserById(@PathVariable("id") Long userId){
// 基于自定义service方法查询
return userService.queryUserAndAddressById(userId);
}
由于查询业务复杂,所以要在service
层来实现。首先在IUserService
中定义方法:
package com.itheima.mp.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.domain.vo.UserVO;
public interface IUserService extends IService<User> {
UserVO queryUserAndAddressById(Long userId);
}
然后,在UserServiceImpl
中实现该方法:
@Override
public UserVO queryUserAndAddressById(Long userId) {
// 1.查询用户
User user = getById(userId);
if (user == null || user.getStatus() == 2){
throw new RuntimeException("用户状态异常");
}
// 2.查询收货地址
List<Address> addresses = Db.lambdaQuery(Address.class).eq(Address::getUserId, userId).list();
// 3.封装VO
// 3.1 user的 po 转 vo
UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
// 3.2 地址的po 转vo
if(CollUtil.isNotEmpty(addresses)){
userVO.setAddresses(BeanUtil.copyToList(addresses, AddressVO.class));
}
return userVO;
}
在查询地址时,我们采用了Db
的静态方法,因此避免了注入AddressService
,减少了循环依赖的风险。
再来实现一个功能:
案例二: 根据
id
批量查询用户,并查询出用户对应的所有地址
修改UserController
中根据id
查询用户的业务接口:
@GetMapping
@ApiOperation("根据id集合查询用户")
public List<UserVO> queryUserByIds(@RequestParam("ids") List<Long> ids){
return userService.queryUserAndAddressByIds(ids);
}
service
层。首先在IUserService
中定义方法:
package com.itheima.mp.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.domain.vo.UserVO;
public interface IUserService extends IService<User> {
List<UserVO> queryUserAndAddressByIds(List<Long> ids);
}
然后,在UserServiceImpl
中实现该方法:
@Override
public List<UserVO> queryUserAndAddressByIds(List<Long> ids) {
// 1.查询用户
List<User> users = listByIds(ids);
if (CollUtil.isEmpty(users)) { // 如果为空 直接返回空列表
return Collections.emptyList();
}
// 2.查询地址
// 2.1. 获取用户id集合
List<Long> userIds = users.stream().map(User::getId).collect(Collectors.toList());
// 2.2. 根据用户id集合 查询地址
List<Address> addresses = Db.lambdaQuery(Address.class).in(Address::getUserId, userIds).list();
// 2.3.转地址VO
List<AddressVO> addressVOList = BeanUtil.copyToList(addresses, AddressVO.class);
// 2.4.用户地址集合 分组处理,一个用户对应一个地址集合,map结构
Map<Long, List<AddressVO>> addressMap = new HashMap<>(0);
if (CollUtil.isNotEmpty(addressVOList)) { // 根据用户id 进行分组
addressMap = addressVOList.stream().collect(Collectors.groupingBy(AddressVO::getUserId));
}
// 3.转VO返回
ArrayList<UserVO> list = new ArrayList<>(users.size());
for (User user : users) {
// 3.1. 转换User 的 po 为 vo
UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
list.add(userVO);
// 3.2. 转换地址的VO
userVO.setAddresses(addressMap.get(user.getId()));
}
return list;
}
3.3.逻辑删除
对于一些比较重要的数据,我们往往会采用逻辑删除的方案,即:
- 在表中添加一个字段标记数据是否被删除
- 当删除数据时把标记置为
true
- 查询时过滤掉标记为
true
的数据
一旦采用了逻辑删除,所有的查询和删除逻辑都要跟着变化,非常麻烦。
为了解决这个问题,MybatisPlus
就添加了对逻辑删除的支持。
注意,只有
MybatisPlus
生成的SQL
语句才支持自动的逻辑删除,自定义SQL
需要自己手动处理逻辑删除。
例如,我们给address
表添加一个逻辑删除字段:
alter table address add deleted bit default b'0' null comment '逻辑删除';
然后给Address
实体添加deleted
字段:
接下来,我们要在application.yml
中配置逻辑删除字段:
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
测试:
首先,我们执行一个删除操作:
@Test
void testDeleteByLogic() {
// 删除方法与以前没有区别
addressService.removeById(59L);
}
方法与普通更新一模一样,但是底层的SQL
逻辑变了:
执行结果如下图:
查询一下试试:
@Test
void testQuery() {
List<Address> list = addressService.list();
list.forEach(System.out::println);
}
会发现id
为59
的确实没有查询出来,而且SQL
中也对逻辑删除字段做了判断:
综上, 开启了逻辑删除功能以后,我们就可以像普通删除一样做CRUD
,基本不用考虑代码逻辑问题。还是非常方便的。
注意:
逻辑删除本身也有自己的问题,比如:
- 会导致数据库表垃圾数据越来越多,从而影响查询效率
- SQL中全都需要对逻辑删除字段做判断,影响查询效率
因此,我不太推荐采用逻辑删除功能,如果数据不能删除,可以采用把数据迁移到其它表的办法。
3.3.通用枚举
User
类中有一个用户状态字段:
像这种字段我们一般会定义一个枚举,做业务判断的时候就可以直接基于枚举做比较。但是我们数据库采用的是int
类型,对应的PO也是Integer
。因此业务操作时必须手动把枚举
与Integer
转换,非常麻烦。
因此,MybatisPlus
提供了一个处理枚举的类型转换器,可以帮我们把枚举类型与数据库类型自动转换。
3.3.1.定义枚举
我们定义一个用户状态的枚举:
代码如下:
package com.itheima.mp.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
import lombok.Getter;
@Getter
public enum UserStatus {
NORMAL(1, "正常"),
FREEZE(2, "冻结")
;
private final int value;
private final String desc;
UserStatus(int value, String desc) {
this.value = value;
this.desc = desc;
}
}
然后把User
类中的status
字段的数据类型由Integer
改为UserStatus
类型:
要让MybatisPlus
处理枚举与数据库类型自动转换,我们必须告诉MybatisPlus
,枚举中的哪个字段的值作为数据库值。
MybatisPlus
提供了@EnumValue
注解来标记枚举属性:
3.3.2.配置枚举处理器
在application.yaml
文件中添加配置:
mybatis-plus:
configuration:
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
3.3.3.测试
@Test
void testService() {
List<User> list = userService.list();
list.forEach(System.out::println);
}
最终,查询出的User
类的status
字段会是枚举类型:
同时,为了使页面查询结果也是枚举格式,我们需要修改UserVO
中的status
属性:
并且,在UserStatus
枚举中通过@JsonValue
注解标记JSON
序列化时展示的字段:
最后,在页面查询,结果如下:
3.4.JSON类型处理器
数据库的user
表中有一个info
字段,是JSON
类型:
格式像这样:
{"age": 20, "intro": "佛系青年", "gender": "male"}
而目前User
实体类中却是String
类型:
这样一来,我们要读取info
中的属性时就非常不方便。如果要方便获取,info
的类型最好是一个Map
或者实体类。
而一旦我们把info
改为对象类型,就需要在写入数据库时手动转为String
,再读取数据库时,手动转换为对象,这会非常麻烦。
因此MybatisPlus
提供了很多特殊类型字段的类型处理器,解决特殊字段类型与数据库类型转换的问题。例如处理JSON
就可以使用JacksonTypeHandler
处理器。
接下来,我们就来看看这个处理器该如何使用。
3.4.1.定义实体
首先,我们定义一个单独实体类来与info
字段的属性匹配:
代码如下:
package com.itheima.mp.domain.po;
import lombok.Data;
@Data
public class UserInfo {
private Integer age;
private String intro;
private String gender;
}
3.4.2.使用类型处理器
接下来,将User
类的info
字段修改为UserInfo
类型,并声明类型处理器:
测试可以发现,所有数据都正确封装到UserInfo
当中了:
同时,为了让页面返回的结果也以对象格式返回,我们要修改UserVO
中的info
字段:
此时,在页面查询结果如下: