MyBatisPlus入门

255 阅读8分钟

如何使用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的查询

需求:

  1. 查询出名字中带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);
    
  2. 更新用户名为jack的用户余额为2000

    UserData user = new UserData();
    user.setBalance(2000);
    QueryWrapper<UserData> wrapper = new QueryWrapper<UserData>()
    	.eq("username","jack");
    userMapper.update(user,wrapper);
    
  3. 更新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语句中剩下的部分

  1. 基于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);
    
  2. 在mapper方法参数中用Param注解声明wrapper变量名称,必须是ew

    void updateBalanceByIds(@Param("ew") LambdaQueryWrapper<UserData> wrapper,@Param("amout") int amout);
    
  3. 自定义SQL,并使用Wrapper条件

    <update id="updateBalanceByIds">
        UPDATE tb_user SET balance = balance - #{amout} ${ew.customSqlSegment}
    </update>
    

需求:

  1. 将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接口

  1. service接口继承IService<实体类>
  2. 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修改用户余额的接口,要求如下:

  1. 完成对用户状态校验
  2. 完成对用户余额校验
  3. 如果扣减后余额为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性能最好

扩展功能

代码生成

  1. 下载MyBatisPlus插件
  2. 点击IDEA菜单栏中其他
    1. 连接数据库jdbc:mysql://localhost:3306/数据库名?serverTimezone=Asia/Shanghai,需要加入时区
    2. 生成代码
  3. 填写根包名,勾选生成选项,配置子包名,勾选需要的注解生成

静态工具

如果有两张表的关联较为紧密,两者的service需要相互注入,会存在循环依赖问题

需求:

  1. 改造根据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;
}
  1. 实体类中加注解@TableName(autoResultMap = true)
  2. 类中属性加注解@TableField(typeHandler = JacksonTypeHandler.class)

插件功能

MyBatisPlus提供的内置拦截器有下面这些:

序号拦截器描述
1TenantLineInnerInterceptor多租户插件
2DynamicTableNameInnerInterceptor动态表名插件
3PaginationInnerInterceptor分页插件
4OptimisticLockerInnerInterceptor乐观锁插件
5IllegalSQLInnerInterceptorSQL性能规范插件,检测并拦截垃圾SQL
6BlockAttackInnerInterceptor防止全表更新和删除的插件

分页插件

首先,要在配置类中注册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;
}