如何使用MyBatisPlus
引入依赖
<!--数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
<!--MP依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.5</version>
</dependency>
启动类注解(非必须,仅在包扫描范围之外需要配置)
@MapperScan("com.bbober.mapper")
Mapper层继承关系
public interface UserMapper extends BaseMapper<UserData> {
}
常见注解
MyBatisPlus通过扫描实体类,并基于反射获取实体类信息作为数据库表信息。
- 类名驼峰转下划线作为表名
- 名为id的字段作为主键
- 变量名驼峰转下划线作为表的字段名
MyBatisPlus中比较常用的几个注解如下:
-
@TableName:用来指定表名
-
@TableId:用来指定表中的主键字段信息
- value:主键名
- type:主键类型
- AUTO数组库自增长
- INPUT通过set方法自行输入
- ASSIGN_ID分配ID接口IdentifierGenerator的方法nextId来生成id,默认实现为雪花算法
-
@TableField:用来指定表中的普通字段信息
- exist值为false代表数据库中没有该字段
注:如果以is开头、'order'的字段一定要加入该注解
常见配置
MyBatisplus的配置项继承了MyBatis原生配置和一些自己特有的配置
mybatis-plus:
type-aliases-package: com.bbober.web.pojo #别名扫描包
mapper-locations: "classpath*:/mapper/**/*.xml" #Mapper.xml文件地址,默认值
configuration:
map-underscore-to-camel-case: true #是否开启下划线和驼峰的映射
cache-enabled: false #是否开启二级缓存
global-config:
db-config:
id-type: assign_id #id为雪花算法生成
update-strategy: not_null #更新策略:只更新非空字段
MyBatisPlus核心功能
条件构造器
MyBatisPlus支持各种复杂的where条件,可以满足日常开发的所有需求
基于QueryWrapper的查询
需求:
-
查询出名字中带o的,存款大于等于1000元的人的id、username、info、balance字段
QueryWrapper<UserData> wrapper = new QueryWrapper<UserData>() .select("id","username","info","balance") .like("username","o") .ge("balance",1000); List<UserData> userData1 = userMapper.selectList(wrapper); -
更新用户名为jack的用户余额为2000
UserData user = new UserData(); user.setBalance(2000); QueryWrapper<UserData> wrapper = new QueryWrapper<UserData>() .eq("username","jack"); userMapper.update(user,wrapper); -
更新id为1,2,4的用户的余额,扣200
List<Long> ids = List.of(1L,2L,4L); UpdateWrapper updateWrapper = new UpdateWrapper<>() .setSql("balance = balance - 200") .in("id",ids);
基于LambdaQueryWrapper的查询
查询语句如下:
List<Long> ids = List.of(1L,2L,4L);
LambdaQueryWrapper<UserData> wrapper = new LambdaQueryWrapper<UserData>()
.in(UserData::getTestId,ids);
需要注意的是,在new后面的泛型中需要传入实体类
自定义sql
我们可以利用MyBatisPlus的Wrapper来构建复杂的where条件,然后自己定义SQL语句中剩下的部分
-
基于Wrapper构建where条件
List<Long> ids = List.of(1L,2L,4L); int amout = 200; LambdaQueryWrapper<UserData> wrapper = new LambdaQueryWrapper<UserData>() .in(UserData::getTestId,ids); userMapper.updateBalanceByIds(wrapper,amout); -
在mapper方法参数中用Param注解声明wrapper变量名称,必须是ew
void updateBalanceByIds(@Param("ew") LambdaQueryWrapper<UserData> wrapper,@Param("amout") int amout); -
自定义SQL,并使用Wrapper条件
<update id="updateBalanceByIds"> UPDATE tb_user SET balance = balance - #{amout} ${ew.customSqlSegment} </update>
需求:
- 将id在指定范围内的用户(例如1、2、4)的余额扣减指定值200
//service层
List<Long> ids = List.of(1L,2L,4L);
int amout = 200;
LambdaQueryWrapper<UserData> wrapper = new LambdaQueryWrapper<UserData>()
.in(UserData::getTestId,ids);
userMapper.updateBalanceByIds(wrapper,amout);
//mapper层逻辑
void updateBalanceByIds(@Param("ew") LambdaQueryWrapper<UserData> wrapper,@Param("amout") int amout);
//xml文件
<update id="updateBalanceByIds">
UPDATE tb_user SET balance = balance - #{amout} ${ew.customSqlSegment}
</update>
Service接口
- service接口继承IService<实体类>
- serviceImpl类实现service接口、继承IServiceImpl<mapper接口,实体类>
IService的Lambda查询
需求:实现一个根据复杂条件查询用户的接口,查询条件如下:
- name:用户名关键字,可以为空
- status:用户状态,可以为空
- minBalance:最小余额,可以为空
- maxBalance:最大余额,可以为空
//Controller层
@GetMapping
public List<UserVO> queryUsers(UserQuery query){
//查询用户PO
List<User> users = userService.queryUsers(
query.getName(),
query.getStatus(),
query.getMinBalance(),
query.getMaxBalance());
//把PO拷贝到VO
return BeanUtil.copyToList(users, UserVO.class);
}
//service层
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)
.gt(minBalance != null, User::getBalance, minBalance)
.lt(maxBalance != null, User::getBalance, maxBalance)
.list();
}
IService的Lambda查询
需求:改造根据id修改用户余额的接口,要求如下:
- 完成对用户状态校验
- 完成对用户余额校验
- 如果扣减后余额为0,则将用户status修改为冻结状态(2)
//Controller层
@PutMapping("/{id}/deduction/{money}")
public void deductMoneyById(
@PathVariable("id") Long id,
@PathVariable("money") Integer money){
userService.deductBalance(id, money);
}
//Service层
@Override
@Transactional
public void deductBalance(Long id, Integer money) {
//1.查询用户
User user = getById(id);
//2.校验用户状态
if (user == null || user.getStatus() == 2){
throw new RuntimeException("用户状态异常");
}
//3.校验余额是否充足
if (user.getBalance() < money){
throw new RuntimeException("用户余额不足!");
}
//4.扣减余额
int remainBalance = user.getBalance() - money;
boolean update = lambdaUpdate()
.set(User::getBalance, remainBalance)
.set(remainBalance == 0, User::getStatus, 2)
.eq(User::getId, id)
.eq(User::getBalance, user.getBalance())
.update();
if (!update){
throw new RuntimeException("扣减金额失败");
}
}
IService批量新增
批量传入10万条用户数据
- 普通for循环插入 需要通过10万次网络IO,网络IO速度较慢
- IService的批量插入
- rewriteBatchedStatements=false 逐条执行,速度慢
- rewriteBatchedStatements=true 一条sql执行,速度快
批处理方案:
- 普通for循环逐条插入速度极差,不推荐
- MP的配置新增基于预编译的批处理,性能不错
- 配置jdbc参数,开rewriteBatchedStatements性能最好
扩展功能
代码生成
- 下载MyBatisPlus插件
- 点击IDEA菜单栏中其他
- 连接数据库jdbc:mysql://localhost:3306/数据库名?serverTimezone=Asia/Shanghai,需要加入时区
- 生成代码
- 填写根包名,勾选生成选项,配置子包名,勾选需要的注解生成
静态工具
如果有两张表的关联较为紧密,两者的service需要相互注入,会存在循环依赖问题
需求:
- 改造根据id查询用户的接口,查询用户时,查询出用户对应的所有地址
//Service层 @Override public UserVO queryUserAndAddressById(Long id) { //1.查询用户 User user = getById(id); if (user == null || user.getStatus() == 2){ 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.setAddressList(BeanUtil.copyToList(addresses, AddressVO.class)); } return userVO; }
逻辑删除
逻辑删除就是基于代码逻辑模拟删除效果,但并不会真正删除数据。思路如下:
- 在表中添加一个字段标记数据是否被删除
- 当删除数据时把标记置为1
- 查询时只查询标记为0的数据
例如逻辑删除字段为deleted:
-
删除操作:
UPDATE user SET deleted = 1 WHERE id = 1 AND deleted = 0 -
查询操作:
SELECT * FROM user WHERE deleted = 0
MyBatsPlus提供了逻辑删除功能,无需改变方法调用的方式,而是在底层帮我们自动修改CRUD的语句。我们要做的就是在application.yml文件中配置逻辑删除的字段名称和值即可:
mybatis-plus:
global-config:
db-config:
logic-delete-field: flag #全局逻辑删除的实体字段名,字段类型可以是boolean、intrger
logic-delete-value: 1 #逻辑已删除值(默认为1)
logic-not-delete-value: 0 #逻辑未删除值(默认为0)
注意:
逻辑删除本身也有自己的问题,比如:
- 会导致数据库表垃圾数据越来越多,影响查询效率
- SQL中全都需要对逻辑删除字段做判断,影响查询效率
因此,不太推荐采用逻辑删除功能,如果数据不能删除,可以采用把数据迁移到其他表的方法。
枚举处理器
User类中有一个用户状态字段:
//详细信息
private String info;
//使用状态
private UserStatus status;
枚举:
@Getter
public enum UserStatus {
NORMAL(1,"正常"),
FREEZE(2,"冻结");
@EnumValue
@JsonValue
private final int value;
private final String desc;
UserStatus(int value,String desc){
this.value = value;
this.desc = desc;
}
}
此时存在一个问题,就是数据库中字段数据类型和Java代码中数据类型不一致,需要进行类型转换,此时就需要使用到枚举处理器,在枚举中的需要与数据库同步的数据通过加入注解@EnumValue指定
然后需要让枚举数据类型生效,需要在配置文件中进行配置
mybatis-plus:
configuration:
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
在使用枚举处理器后,前端返回类型会返回枚举字段数据,不便于调试,需要在需要返回的返回值上加入SpringMVC的序列化器中的注解@JsonValue
JSON处理器
数据库表中user表中有一个json类型的字段
//需要json序列化/反序列化的对象
@Data
@NoArgsConstructor
@AllArgsConstructor(staticName = "of")
public class UserInfo {
private Integer age;
private String intro;
private String gender;
}
//数据库实体
@Data
@TableName(value = "user",autoResultMap = true)
public class User {
private Long id;
private String username;
private String password;
private String phone;
@TableField(typeHandler = JacksonTypeHandler.class)
private UserInfo info;
private UserStatus status;
private Integer balance;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
- 实体类中加注解@TableName(autoResultMap = true)
- 类中属性加注解@TableField(typeHandler = JacksonTypeHandler.class)
插件功能
MyBatisPlus提供的内置拦截器有下面这些:
| 序号 | 拦截器 | 描述 |
|---|---|---|
| 1 | TenantLineInnerInterceptor | 多租户插件 |
| 2 | DynamicTableNameInnerInterceptor | 动态表名插件 |
| 3 | PaginationInnerInterceptor | 分页插件 |
| 4 | OptimisticLockerInnerInterceptor | 乐观锁插件 |
| 5 | IllegalSQLInnerInterceptor | SQL性能规范插件,检测并拦截垃圾SQL |
| 6 | BlockAttackInnerInterceptor | 防止全表更新和删除的插件 |
分页插件
首先,要在配置类中注册MyBatisPlus的核心插件,同时添加分页插件
@Configuration
public class MyBatisConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
//1.初始化核心插件
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//2.添加分页插件
PaginationInnerInterceptor pageInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
//设置分页上限
pageInterceptor.setMaxLimit(1000L);
interceptor.addInnerInterceptor(pageInterceptor);
return interceptor;
}
}
接着,就可以使用分页的API了
void testpageQuery(){
//1.查询
int pageNo = 1, pageSize = 5;
//1.1分页参数
Page<User> page = Page.of(pageNo,pageSize);
//1.2排序参数
page.addOrder(new OrderItem().setColumn("balance").setAsc(false));
//1.3分页查询
Page<User> p = userService.page(page);
//2.总条数
System.out.println("total:" + p.getTotal());
//3.总页数
System.out.println("pages:" + p.getPages());
//4.分页数据
List<User> records = p.getRecords();
records.forEach(System.out::println);
}
通用分页实体
需求:遵循下面的接口规范,编写一个UserController接口,实现User的分页查询
| 参数 | 说明 |
|---|---|
| 请求方式 | GET |
| 请求路径 | /user/page |
| 请求参数 | { "pageNo":1, "pageSize":5, "sortBy":"balance", "isAsc":false, "name":"jack", "status":1 } |
| 返回值 | -- |
| 特殊说明 | 1.如果排序字段为空,默认按照更新时间排序 2.排序字段不为空,则按照排序字段排序 |
//service
@Override
public PageDTO<UserVO> queryUsersPage(UserQuery query) {
String name = query.getName();
Integer status = query.getStatus();
//1.构建查询条件
//1.1分页条件
Page<User> page = Page.of(query.getPageNo(), query.getPageSize());
//1.2排序条件
if (query.getSortBy() != null){
page.addOrder(new OrderItem().setColumn(query.getSortBy()).setAsc(query.getIsAsc()));
}else {
page.addOrder(new OrderItem().setColumn("update_time").setAsc(false));
}
//2.分页查询
Page<User> p = lambdaQuery()
.like(name != null, User::getUsername, name)
.eq(status != null, User::getStatus, status)
.page(page);
//3.封装VO结果
PageDTO<UserVO> dto = new PageDTO<>();
dto.setTotal(p.getTotal());
dto.setPages(p.getPages());
dto.setList(BeanUtil.copyToList(p.getRecords(),UserVO.class));
//4.返回
return dto;
}