[MyBatis-Plus]扩展功能详解
代码生成
使用MP的步骤是非常固定的几步操作
基于插件, 可以快速的生成基础性的代码
- 安装插件
- 安装完成后重启IEDA
- 连接数据库
- mp是数据库的名字
- ?serverTimezone=UTC 是修复mysql时区, 不加会报错
- 生成代码
- TablePrefix选项是用于去除表名的前缀, 比如根据tb_user表生成实体类User, 就填写tb_
静态工具
业务开发中, 可能会存在service之间互相调用, 如果使用传统的资源注入, 就会出现循环依赖的问题
建议就是service之间的相互调用, 使用DB静态工具进行方法的调用, 避免循环依赖的问题
DB静态工具的方法和IService的方法几乎一样, 只是调用时需要传入字节码
案例1: 改造根据id查询用户的接口, 查询用户的同时, 查询出对应的地址
/**
* @author
* @since 2023-07-01
*/
@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;
}
@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("根据id查询用户接口")
@GetMapping("/{id}")
public UserVO queryUserById(@ApiParam("用户id") @PathVariable("id") Long id){
return userService.queryUserAndAddressById(id);
}
}
public interface IUserService extends IService<User> {
UserVO queryUserAndAddressById(Long id);
}
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Override
public UserVO queryUserAndAddressById(Long id) {
//1,查询用户
User user = getById(id);
if (user == null || user.getStatus() == UserStatus.FROZEN) {
throw new RuntimeException("用户状态异常");
}
//2查询地址
List<Address> addresses = Db.lambdaQuery(Address.class).eq(Address::getUserId, id).list();
//3封装VO
//3.1把user的PO转成VO
UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
//3.2转地址为VO
if (CollUtil.isNotEmpty(addresses)) {
userVO.setAddresses(BeanUtil.copyToList(addresses, AddressVO.class));
}
return userVO;
}
}
案例2: 改造根据id批量查询的接口, 查询用户的同时, 查询出用户对应的所有地址
@Api(tags = "用户管理接口")
@RequestMapping("/users")
@RestController
@RequiredArgsConstructor
public class UserController {
private final IUserService userService;
@ApiOperation("根基id批量查询用户接口")
@GetMapping
public List<UserVO> queryUserByIds(@ApiParam("用户id集合") @RequestParam("ids") List<Long> ids) {
return userService.queryUserAndAddressByIds(ids);
}
}
public interface IUserService extends IService<User> {
List<UserVO> queryUserAndAddressByIds(List<Long> ids);
}
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Override
public List<UserVO> queryUserAndAddressByIds(List<Long> ids) {
//1,查询用户
List<User> users = listByIds(ids);
if (CollUtil.isNotEmpty(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<Long, List<AddressVO>> addressMap = new HashMap<>(0);
if (CollUtil.isNotEmpty(addressVOList)) {
addressMap = addressVOList.stream().collect(Collectors.groupingBy(AddressVO::getUserId));
}
//3,转换VO返回
List<UserVO> list = new ArrayList<>(users.size());
for (User user : users) {
//3.1转换USER的PO为VO
UserVO vo = BeanUtil.copyProperties(user, UserVO.class);
list.add(vo);
//3.2转换地址VO
vo.setAddresses(addressMap.get(user.getId()));
}
return list;
}
}
逻辑删除
逻辑删除就是基于代码逻辑, 模拟删除效果, 不会真正删除数据
- 在表中添加一个字段标记数据是否被删除
- 当删除数据时把标记置为1
- 查询时只查询标记为0的数据
MP提供了逻辑删除功能, 无需改变方法调用的方式, 只需要简单配置, MP就会在底层自动修改CRUE的语句
- 在application.yaml文件中配置逻辑删除的字段名称和值就可以
案例
mybatis-plus:
type-aliases-package: com.itheima.mp.domain.po
global-config:
db-config:
logic-delete-field: deleted # 配置逻辑删除字段
@SpringBootTest
class IAddressServiceTest {
@Autowired
private IAddressService addressService;
@Test
void testLogicDelete() {
//1,删除
addressService.removeById(59L); // 代码是正常删除,但执行的是更新操作,更新deleted字段
//2,查询
Address address = addressService.getById(59L);
System.out.println("address=" + address); // null,已经删除的数据查不到
}
}
逻辑删除存在的问题:
- 会导致数据表中的垃圾数据越来越多, 影响查询效率
- SQL中全都要对逻辑删除字段进行判断,影响查询效率
其他方案
- 如果数据不能删除, 可以把在删除之前, 把数据迁移到备份的表中,
- 然后执行删除操作, 这样主表中的数据就比较干净, 而且数据也会保留
枚举处理器
作用: 解决java的enum类型和数据库INT类型的相互转换问题
- 开发过程中, 我们会使用枚举类型 替换 固定数值
- 但数据表中存储的字段依然是整数类型
- 所以java实体类中和数据库中数据类型要相互转换
原理: MP提供了枚举类型处理器, 用于自动转换实体类的枚举类型
- 存储数据: 自动把枚举类型转成INT类型存入数据库
- 封装数据: 自动把数据库INT类型数据转成枚举类型保存
配置全局枚举处理器, 让枚举处理器生效
mybatis-plus:
configuration:
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
给枚举类添加注解, 告诉MP要对哪个成员变量进行类型处理
@Getter
public enum UserStatus {
NORMAL(1, "正常"),
FROZEN(2, "冻结"),
;
@EnumValue // 指定写入数据库的数据
private final int value;
@JsonValue // 指定返回前端的数据
private final String desc;
UserStatus(int value, String desc) {
this.value = value;
this.desc = desc;
}
}
- 枚举做JSON处理时, 默认会以枚举项的名字返回
- 如果想以value或者desc返回, 可以通过注解告诉SpringMVC要返回那个值
在实体类中使用枚举类型
@Data
@ApiModel(description = "用户VO实体")
public class UserVO {
... ...
@ApiModelProperty("使用状态(1正常 2冻结)")
// private Integer status;
private UserStatus status;
}
@Data
@TableName(value = "user",autoResultMap = true)
public class User {
... ...
/**
* 使用状态(1正常 2冻结)
*/
private UserStatus status;
}
业务代码中, 愉快的使用枚举替代魔法数值
@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();
}
}
JSON处理器
问题说明
- user表中有一个info字段是json类型, java中没有对应的数据类型, 通常用字符串类型接收
- MuBatis可以自动进行字符串和JSON的转换
- 使用字符串接收JSON类型数据, 从数据库的操作层面是没有问题, 增删改查都是正常的
- 但是从业务层面就会出现麻烦, 比如需要取出info中的age信息, 处理起来就比较麻烦
MP提供了JSON类型处理器, 可以把java对象和数据库的JSON对象 相互转换
- SpringMVC使用的JacksonJSON处理器
- 使用JacksonTypeHandler不需要额外引用依赖
使用JSON处理器的步骤
- 使用 @TableField() 注解, 指定并开启JSON处理器
- 使用JSON数据后, 就出现了对象嵌套对象的情况, User对象嵌套UserInfo对象
- 就要开启自动结果映射 autoResultMap = true
实际操作
@Data
@NoArgsConstructor
@AllArgsConstructor(staticName = "of")
public class UserInfo {
private Integer age;
private String intro;
private String gender;
}
- 定义PO对象封装JSON数据
@Data
@TableName(value = "user",autoResultMap = true)
public class User {
... ...
/**
* 详细信息
*/
@TableField(typeHandler = JacksonTypeHandler.class)
// private String info;
private UserInfo info;
}
- 使用java对象替代String接收数据库数据
- 使用@TableField()注解开启JSON处理器
- 设置autoResultMap = true, 开启自动结果映射
- 查询结果对比