在学习了 MyBatis Plus 的使用之后,我们发现了基础的 Mapper
、Service
、PO
等等代码基本上都是固定的,如果这样的话重复的编写代码就显得非常麻烦了。恰好,MyBatis Plus 官方就提供了代码生成器来根据数据库的表结构来自动为我们生成 Mapper
、Service
、PO
相关的代码。只不过代码生成器同样要编码使用,也很麻烦。这里推荐大家使用一款 Mybatis Plus 的插件,它可以基于图形化界面完成 Mybatis Plus 的代码生成,非常简单。
在 IDEA 的 plugins 中搜索 “Mybatis Plus”,选择其中那个图标特别可爱的就是了:
安装成功之后,可以在 IDEA 的导航栏中发现看到一个 Orther
选项:
其中就包括了数据库的配置以及生成代码的选项了。
此时正好我们有一个 address
表还没有编写对应的代码的,此时我们可以使用这个插件来自动生成 address
表相关的代码。
other
,然后选择生成代码:点击 “code generatro” ,就会自动生成代码到指定位置。
有的时候不同的 Service 类之间会相互调用,为了避免出现循环依赖问题,Mybatis Plus 提供一个静态工具类:Db
,其中的一些静态方法与IService
中方法签名基本一致,也可以帮助我们实现 CRUD 的功能:
例如,下面的使用实例:
/**
* 获取id为 1 的用户信息
*/
@Test
void testDbGet() {
User user = Db.getById(1L, User.class);
System.out.println(user);
}
/**
* 查询用户名中带 “o”, 并且balance >= 1000 的用户信息
*/
@Test
void testDbList() {
List<User> list = Db.lambdaQuery(User.class)
.like(User::getUsername, "o")
.ge(User::getBalance, 1000).list();
System.out.println(list);
}
/**
* 设置用户名为 Rose 的用户的 balance 为 2500
*/
@Test
void testDbUpdate() {
Db.lambdaUpdate(User.class)
.set(User::getBalance, 2500)
.eq(User::getUsername, "Rose").update();
}
可以发现,对应 Db
静态类的使用和前面的使用方法都是类似的。
示例一:改造根据 id 用户查询的接口,查询用户的同时返回用户收货地址列表。
@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 id) {
return userService.queryUserAndAddressById(id);
}
此时,新增了一个queryUserAndAddressById
方法。
在 service
层实现queryUserAndAddressById
方法
IUserService
中定义方法:public interface IUserService extends IService<User> {
UserVO queryUserAndAddressById(Long id);
}
UserServiceImpl
中实现该方法:@Override
public UserVO queryUserAndAddressById(Long id) {
// 1. 查询用户
User user = getById(id);
// 2. 使用 Db 根据用户查询地址类别
List<Address> addresses = Db.lambdaQuery(Address.class)
.eq(Address::getUserId, user.getId())
.list();
// 3. 处理 VO
UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
userVO.setAddresses(BeanUtil.copyToList(addresses, AddressVO.class));
return userVO;
}
在查询地址时,使用用了 Db
中的静态方法,因此避免了注入 AddressService
,减少了循环依赖的风险。
完成上上面的代码之后,通过 id
查询的用户信息中就有了地址信息了:
示例二:改造根据 id 批量查询用户的接口,要求查询出用户对应的所有地址
controller
接口:@GetMapping
@ApiOperation("根据id批量查询用户接口")
public List<UserVO> queryUserByIds(@ApiParam("用户id集合") @RequestParam("ids") List<Long> ids) {
return userService.queryUserAndAddressByIds(ids);
}
在 service
层实现queryUserAndAddressByIds
方法
IUserService
中定义方法:public interface IUserService extends IService<User> {
List<UserVO> queryUserAndAddressByIds(Long id);
}
UserServiceImpl
中实现该方法:@Override
public List<UserVO> queryUserAndAddressByIds(List<Long> ids) {
// 1. 查询用户集合
List<User> users = this.listByIds(ids);
if (users.isEmpty()) {
return Collections.emptyList();
}
// 2. 查询地址
// 2.1 获取用户id
List<Long> userIds = users.stream().map(User::getId).collect(Collectors.toList());
// 2.2 查询地址
List<Address> addresses = Db.lambdaQuery(Address.class).in(Address::getUserId, userIds).list();
// 2.3 地址转换为 VO
List<AddressVO> addressVOS = BeanUtil.copyToList(addresses, AddressVO.class);
// 2.4 按照 userId 将地址 VO 进行分组
Map<Long, List<AddressVO>> addressesMap = new HashMap<>();
if (CollUtil.isNotEmpty(addresses)) {
addressesMap = addressVOS.stream().collect(Collectors.groupingBy(AddressVO::getUserId));
}
// 3. 转换 VO 返回
List<UserVO> list = new ArrayList<>(users.size());
for (User user : users) {
UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
// 填充地址
userVO.setAddresses(addressesMap.get(user.getId()));
list.add(userVO);
}
return list;
}
注意事项:
在使用查询到的用户的id去查询地址信息的时候,要避免在循环中查询数据库。因此首先获取用户 id 集合,然后再根据这些 id 集合批量查询地址信息。
将查询出的地址信息按照用户 id 进行分类,然后设置进对应的 UserVO
对象中。
对于一些比较重要的数据,我们往往会采用逻辑删除的方案,例如:
在表中添加一个字段标记数据是否被删除
当删除数据时把标记置为true
查询时过滤掉标记为true的数据
一旦采用了逻辑删除,所有的查询和删除逻辑都要跟着变化,那么对应数据库的操作就会变得非常麻烦。幸运的是,为了解决这个麻烦,MyBatis Plus 就提供了对逻辑删除的支持。
注意,只有 Mybatis Plus 生成的SQL语句才支持自动的逻辑删除,自定义 SQL 还是需要自己手动处理逻辑删除。
下面演示使用 MyBatis Plus 的逻辑删除功能:
address
表添加一个逻辑删除字段:alter table address add deleted bit default b'0' null comment '逻辑删除';
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted # 逻辑删除字段
logic-delete-value: 1 # 当 deleted 的值为 1,就逻辑删除了
logic-not-delete-value: 0 # 当 deleted 的值为 0 ,没有逻辑删除
当完成了上面所有的准备工作之后,我们可以执行一个删除的测试方法:
@Test
void testLogicDelete() {
addressService.removeById(59L);
}
运行这段代码:
发现将 id 为 59 的地址信息的 deleted
字段设置为了1,如果此时再查询这条数据:
因此开启了逻辑删除功能以后,我们就可以像普通删除一样做 CRUD,基本不用考虑代码逻辑问题。还是非常方便的。但是使用逻辑删除也存在一定的问题,比如:
因此,不太推荐采用逻辑删除功能,如果数据不能删除,可以采用把数据迁移到其它表的办法。
在 User
实体类中有一个用户状态字段:
像这种字段我们一般会定义一个枚举,做业务判断的时候就可以直接基于枚举做比较。但是我们数据库采用的是 int
类型,对应的 PO 也是Integer
。因此业务操作时必须手动把枚举与 Integer
转换,非常麻烦。因此,Mybatis Plus提供了一个处理枚举的类型转换器,可以帮我们把枚举类型与数据库类型自动转换。
首先,我们为用户表中的这个状态字段定义一个枚举常量:
@Getter
public enum UserStatus {
NORMAL(1, "正常"),
FROZE(2, "冻结"),
;
private final int value;
private final String desc;
UserStatus(int value, String desc) {
this.value = value;
this.desc = desc;
}
}
然后把 User
类中的 status
字段改为 UserStatus
类型:
要让 Mybatis Plus处理枚举与数据库类型自动转换,我们必须告诉 Mybatis Plus,枚举中的哪个字段的值作为数据库值。Mybatis Plus 提供了 @EnumValue
注解来标记枚举属性:
在application.yml
文件中添加以下配置,以开启枚举处理器的功能:
mybatis-plus:
configuration:
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler # 枚举处理器
例如,根据id查询某个用户:
此时,查询出的 User
类的 status
字段会是枚举类型。
同时,为了使页面查询结果也是枚举格式,我们需要修改 UserVO
中的status
属性:
并且,在UserStatus
枚举中通过@JsonValue
注解标记 JSON 序列化时展示的字段是 desc
:
数据库的user
表中有一个info
字段,是 JSON 类型:
格式就像这样:
{"age": 20, "intro": "佛系青年", "gender": "male"}
但是目前User
实体类中却是String
类型,因为在 Java 中没有 JSON 这样的类型:
这样一来,我们要读取 info
中的属性时就非常不方便。如果要方便获取,info
的类型最好是一个 Map 或者实体类。
而一旦我们把info
改为对象类型,就需要在写入数据库时手动转为 String,再读取数据库时,手动转换为对象,这会非常麻烦。
因此 Mybatis Plus提供了很多特殊类型字段的类型处理器,解决特殊字段类型与数据库类型转换的问题。例如处理 JSON 就可以使用 JacksonTypeHandler
处理器。
接下来,我们就来看看这个处理器该如何使用。
首先,我们定义一个单独实体类来与 info
字段的属性匹配:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserInfo {
private Integer age;
private String intro;
private String gender;
}
接下来,将 User
类的 info
字段修改为 UserInfo
类型,并声明类型处理器:
注意,需要设置autoResultMap
为 true
,才能生效。