MybatisPlus
对于 Mybatis 无侵入
引入依赖后,定义 Mapper 接口后继承 BaseMapper<User> 就能自动获得 CRUD 方法
通过反射获取实体类信息,作为数据库表信息
MP 基础功能
配置
不遵循约定时要自己配置信息
条件构造器
MP 支持复杂 where 条件
【例子】
查询出名字中带 o 的,存款大于 2000 的用户的 id,username,info,balance
1 2 3 4 5 6 7 8 9
| void testQueryWrapper() { QueryWrapper<User> queryWrapper = new QueryWrapper<>() .select("id", "username", "info", "balance") .like("username", "o") .ge("balance", 2000) List<User> users = userMapper.selectList(queryWrapper); }
|
将其金额更新为 1000
1 2 3 4 5 6 7 8 9 10
| void testUpdateWrapperByQueryWrapper() { User user = new User(); user.setBalance(1000); QueryWrapper<User> queryWrapper = new QueryWrapper<>() .eq("username", "jack"); userMapper.update(user, queryWrapper); }
|
将 id 为 1、2、4 的用户金额扣两百
用 setSql 可以直接手写 sql 语句
1 2 3 4 5 6
| void testUpdateWrapper() { UpdateWrapper<User> updateWrapper = new UpdateWrapper<>() .setSql("balance = balance - 200"); .in("id", 1, 2, 4); userMapper.update(null, updateWrapper); }
|
Lambda 语法动态构造查询条件
1 2 3 4 5 6 7 8 9 10
| void testLambdaQueryWrapper() { LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>() .select(User::getId, User::getUsername, User::getInfo, User::getBalance) .like(User::getUsername, "o") .ge(User::getBalance, 2000) List<User> users = userMapper.selectList(queryWrapper); }
|
自定义 SQL
利用 MybatisPlus 提供的 Wrapper 来构造复杂的 where 条件,然后自己定义 SQL 剩下的部分
【例子】
将 id 为 1、2、4 的用户金额扣两百
在业务层基于 Wrapper 构建 where 条件
1 2 3 4 5 6 7 8 9
| void testCustomSqlUpdate() { List<Long> ids = List.of(1L, 2L, 4L); int amount = 200; QueryWrapper<User> queryWrapper = new QueryWrapper<>().in("id", ids); userMapper.updateBalance(queryWrapper, amount); }
|
在 Mapper 中方法参数中用 Param 注解声明 wrapper 变量名称,必须是 ew
1 2 3 4 5
| public interface UserMapper extends BaseMapper<User> { List<User> queryUserByIds(@Param("ids") List<Long> ids);
void updateBalanceByIds(@Param("ew") QueryWrapper<User> queryWrapper, @Param("amount") int amount); }
|
在 xml 中自定义 SQL 方法,并使用 wrapper 条件
1 2 3 4
| 后面的where条件会被ew.getSqlSegment()替换掉 <update id="updateBalanceByIds"> UPDATE tb_user SET balance = balance - #{amount} ${ew.customSqlSegment} </update>
|
MP 核心功能
Service 接口
Service 接口继承 IService 接口,实现类继承 ServiceImpl 类
然后就有了很多常用的 CRUD 方法,可以直接用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public interface IUserService extends IService<User> {
}
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
}
class UserServiceTest { void testSaveUser() { User user = new User(); user.setUsername("jack"); user.setInfo("hello world");
userService.save(user); } }
|
IService 的 lambda 方法
IService 接口提供了 lambda 方法,可以更方便使用
比如 lambdaUpdate()、lambdaQuery()
更新操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public void deductBalance(Long id, Integer amount) { User user = getById(id); if (user == null || user.getStatus() == 2) { throw new RuntimeException("用户状态异常") } if (user.getBalance() < amount) { throw new RuntimeException("用户余额不足") } int remainBalance = user.getBalance() - amount; lambdaUpdate() .set(User::getBalance, remainBalance) .set(remainBalance == 0, User::getStatus, 2) .eq(User::getId, id) .eq(User::getBalance, user.getBalance()) .update(); }
|
查询操作
1 2 3 4 5 6 7 8
| public List<User> queryUsers(String name, Integer status, Integer minBalance, Integer maxBalance) { return lambdaQuery() .like(name != null, User::getUsername, name) .eq(status != null, User::getStatus, status) .ge(minBalance != null, User::getBalance, minBalance) .le(maxBalance != null, User::getBalance, maxBalance) .list(); }
|
批量插入设置(批处理)
- 假设插入十万条数据,一条一条插会很慢
- 可以每一千条组成一个 list 批量插入,IService 的批量插入 userService.saveBatch(list)
- 但是 MySql 执行的语句还是一条一条的 sql,将 yaml 文件中 jdbc 参数
rewriteBatchedStatements设置为 true,MySql 就能将一组 sql 变成一条 sql
MP 拓展功能
代码生成
MybatisX 或 MybatisPlus 插件都可以生成代码
MybatisPlus 的代码生成器在工具栏里面的 code generation
静态工具类
同时查两个表,Service 之间相互调用会出现循环依赖
这时候用 Db 静态工具类,可以避免,它的方法和 Service 一样,只是需要多传一个字节码
1 2 3 4
| Db.lambdaUpdate(Address.class) .eq(Address::getUserId, id) .list()
|
逻辑删除
在表中添加一个字段标记数据是否被删除
逻辑删除的语句UPDATE address SET deleted = 1 WHERE id = ? AND deleted = 0
MP 提供了逻辑删除功能,无需改变方法调用的方式,只需在 yaml 文件中配置即可
1 2 3 4
| mybatis-plus: global-config: db-config: logic-delete-field: deleted
|
逻辑删除有自己的问题,比如垃圾数据越来越多,每次都要判断,影响效率
所以其实不推荐用逻辑删除,如果数据不能删可以把它迁移到其他表
枚举处理器
如果用 01 等直接代表状态,可能一多就容易混淆
用枚举类的字段来对应 01 状态就更加清晰
但最好实体类的字段类型直接写成枚举类,转换交给 MP 来做,增强可读性
枚举类写入数据库时转成 int 值时是由 MP 的枚举类型转换器来转换的
枚举类型转换器配置
1 2
| configuration: default-enum-type-handler: com.baomidou.mybatisplus.extension.handlers.MybatisEnumTypeHandler
|
定义枚举类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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 处理器
将 JSON 类型的字段转成对象类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public class UserInfo { private Integer age; private String intro; private String gender; }
@Data @TableName(value = "user", autoResultMap = true) public class User {
private String username; private String phone;
@TableField(typeHandler = JacksonTypeHandler.class) private UserInfo info; }
user.setInfo(new UserInfo(18, "hello world", "男")); user.setInfo(UserInfo.of(18, "hello world", "男"));
|
分页插件
首先在配置类中注册 MyBatisPlus 核心插件,同时添加分页插件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Configuration public class MybatisConfig {
@Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); PageinationInnerInterceptor pageInterceptor = new PageinationInnerInterceptor(DbType.MYSQL); pageinationInnerInterceptor.setMaxLimit(1000L); interceptor.addInnerInterceptor(pageInterceptor); return interceptor; } }
|
接着就可以使用分页的 API 了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| void testPageQuery() { int pageNo = 1, pageSize = 10; Page<User> page = Page.of(pageNo, pageSize); page.addOrder(new OrderItem("balance", true)); page.addOrder(new OrderItem("id", true)); Page<User> p = userService.page(page); System.out.println("total = " + p.getTotal()); System.out.println("pages = " + p.getPages()); List<User> records = p.getRecords(); records.forEach(System.out::println) }
|
通用分页实体
定义一个实体类存放分页的参数,其他类只需要继承这个类就行,不用每次都写分页参数了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| @Data @ApiModel(description = "分页查询实体") public class PageQuery { @ApiModelProperty(value = "页码") private Integer pageNo; @ApiModelProperty(value = "页大小") private Integer pageSize; @ApiModelProperty(value = "排序字段") private String sortBy; @ApiModelProperty(value = "是否升序") private Boolean isAsc; }
@Data @ApiModel(description = "用户查询条件实体") class UserQuery extends PageQuery { @ApiModelProperty(value = "用户名") private String username; }
@Data @ApiModel(description = "分页查询结果") public class PageDTO<T> { @ApiModelProperty(value = "总条数") private Long total; @ApiModelProperty(value = "总页数") private Long pages; @ApiModelProperty(value = "分页数据") private List<T> records; }
|
每次分页都要写的逻辑可以直接写在分页查询实体里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| @Data @ApiModel(description = "分页查询实体") public class PageQuery { @ApiModelProperty(value = "页码") private Integer pageNo = 1; @ApiModelProperty(value = "页大小") private Integer pageSize = 5; @ApiModelProperty(value = "排序字段") private String sortBy; @ApiModelProperty(value = "是否升序") private Boolean isAsc = true;
public <T> Page<T> toMpPage(OrderItem ... items) { Page<T> page = new Page<>(pageNo, pageSize); if (StrUtil.isNotBlank(sortBy)) { page.addOrder(new OrderItem(sortBy, isAsc)); } else if (items != null) { page.addOrder(items); } return page; }
public <T> page<T> toMpPage(String defaultSortBy, Boolean defaultAsc) { return toMpPage(defaultSortBy, defaultAsc); } public <T> Page<T> toMpPageDefaultSortByCreateTime() { return toMpPage(new OrderItem("create_time", false)); } public <T> Page<T> toMpPageDefaultSortByUpdateTime() { return toMpPage(new OrderItem("update_time", false)); } }
|
每次把查询结果封装成 DTO 的逻辑也能直接写在 DTO 类里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| @Data @ApiModel(description = "分页查询结果") public class PageDTO<T> { @ApiModelProperty(value = "总条数") private Long total; @ApiModelProperty(value = "总页数") private Long pages; @ApiModelProperty(value = "分页数据") private List<T> records;
public static <PO, VO> PageDTO<VO> of(Page<PO> p, Class<VO> clazz) { PageDTO<VO> dto = new PageDTO<>(); dto.setTotal(p.getTotal()); dto.setPages(p.getPages()); List<PO> records = p.getRecords(); if (CollUtil.isEmpty(records)) { dto.setList(Collections.emptyList()) return dto; } dto.setList(BeanUtil.copyToList(records, clazz)); return dto; }
public static <PO, VO> PageDTO<VO> of(Page<PO> p, Function<PO, NO> convertor) { PageDTO<VO> dto = new PageDTO<>(); dto.setTotal(p.getTotal()); dto.setPages(p.getPages()); List<PO> records = p.getRecords(); if (CollUtil.isEmpty(records)) { dto.setList(Collections.emptyList()) return dto; } dto.setList(records.stream().map(convertor).collect(Collectors.toList())); return dto; } }
|
使用示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| public PageDTO<UserVO> query(UserQuery query) { Strign name = query.getUsername(); Integerr status = query.getStatus(); Page<User> page = query.toMpPageDefaultSortByUpdateTime();
Page<User> p = lambdaQuery() .like(name != null, User::getName, name) .eq(status != null, User::getStatus, status) .page(page);
return PageDTO.of(p, UserVO.class)
return PageDTO.of(p,user -> BeanUtil.copyProperties(user, UserVO.class))
return PageDTO.of(p,user -> { UserVO vo = BeanUtil.copyProperties(user, UserVO.class); vo.setUsername(vo.getUsername().substring(0, vo.getUsername().length() - 2) + "**") }) }
|
Docker