前言
首先它是国产的,所以直接用官网的简介。
- 简介
MyBatis-Plus 是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
- 特性
- 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
- 损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作
- 强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求
- 支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错
支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题 - 支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作
- 支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )
- 内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用
- 内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询
- 分页插件支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库
- 内置性能分析插件:可输出 SQL 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询
- 内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作
- 支持数据库
- 框架结构
好了,上面的都来自于官网,我们要做的就是把官网中各种零散的教程,整合成一个个通俗易懂的案例。
使用步骤
拢共就两步
- 引入MybatisPlus依赖,代替Mybatis依赖
<!-- mybatis依赖直接移除 --><!-- <dependency>--><!-- <groupId>org.mybatis.spring.boot</groupId>--><!-- <artifactId>mybatis-spring-boot-starter</artifactId>--><!-- <version>3.x.xx</version>--><!-- </dependency>--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.10.1</version></dependency>
- 定义Mapper接口并继承BaseMapper
@Mapper
public interface UserMapper extends BaseMapper<User> {}
快速入门-入门案例
需求:基于课前资料提供的项目,实现下列功能:
- 新增用户功能
- 根据id查询用户
- 根据id批量查询用户
- 根据id更新用户
- 根据id删除用户
入门准备
- 创建一个springboot项目
详情参考SpringBootWeb快速入门。
- 创建mysql用户表
create table user
(id bigint primary key auto_increment,username varchar(50),password varchar(50),phone varchar(50),status int default 1,balance int,info varchar(255),create_time datetime,insert_time datetime
)
- 创建对应的实体类
package com.example.javawebdemo.entity;import lombok.Data;import java.time.LocalDateTime;@Data
public class User {private Long id;private String username;private String password;private String phone;private String info;private Integer status;private Integer balance;private LocalDateTime createTime;private LocalDateTime insertTime;
}
- 创建数据库配置文件
spring:datasource:driver-class-name: com.mysql.cj.jdbc.Driverusername: rootpassword: rooturl: jdbc:mysql://localhost:3306/demo
为了可以在控制台日志中看到打印SQL语句,其实我们可以设置的,在application.yml中添加
mybatis-plus:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mybatisplus_142">套用mybatis-plus上面两个步骤
- 引入依赖
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.10.1</version>
</dependency>
- 定义Mapper接口并继承BaseMapper
@Mapper
public interface UserMapper extends BaseMapper<User> {}
BaseMapper定义了大量的增删改查方法,我们继承它就实现了 user 表的 CRUD 功能,甚至连 XML 文件都不用编写!
这里还需要在启动类上面加一个@MapperScan注解,来标注mapper包的位置
package com.example.javawebdemo;import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@MapperScan("com.example.javawebdemo.mapper")
@SpringBootApplication
public class JavawebDemoApplication {public static void main(String[] args) {SpringApplication.run(JavawebDemoApplication.class, args);}
}
编写测试类实现入门案例
- 新增用户
package com.example.javawebdemo;import com.example.javawebdemo.mapper.UserMapper;
import com.example.javawebdemo.entity.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;@SpringBootTest
class JavawebDemoApplicationTests {@Autowiredprivate UserMapper userMapper;@Testpublic void insertUser() {User user = new User();user.setId(1L);user.setUsername("Jack");user.setPassword("123456");user.setPhone("13111111111");user.setInfo("{\"age\":20,\"gender\":\"male\"}");user.setStatus(1);user.setBalance(200);user.setCreateTime(LocalDateTime.now());user.setInsertTime(LocalDateTime.now());userMapper.insert(user);}
}
可以看到我们根本没有配置xml,甚至连sql语句都没写就实现了插入数据的功能。
- 根据id查询用户
@Testpublic void selectUserById() {User user = userMapper.selectById(1);System.out.println(user);}
- 根据id批量查询用户
目前表中只有1条数据,我们再插入id=2的一条数据,来测试批量查询:
@Testpublic void selectUserByIds() {List<User> users = userMapper.selectByIds(Arrays.asList(1, 2));for (User user : users) {System.out.println(user);}}
- 根据id更新用户
@Testpublic void updateUserById() {User user = new User();user.setId(1);user.setBalance(20000);userMapper.updateById(user);}
根据执行日志,可以看出会根据id只更新balance字段:
- 根据id删除用户
@Testpublic void deleteUserById() {userMapper.deleteById(1);}
就这样单表的增删改查就完成了,的确是省事啊。但是它是怎么知道访问哪张表,以及表里有哪些字段的呢?继续往下看…
快速入门-常见注解
MyBatisPlus通过扫描实体类,并基于反射获取实体类信息作为数据库表信息。
并且有下面的约定:
- 类名驼峰转下划线作为表名
- 名为id的字段作为主键
- 变量名驼峰转下划线作为表的字段名
所以我们在写实体类的时候尽量根据表的DDL来创建,简单来说约定大于配置。
但是如果我们实体类不符合这些约定怎么办?
不符合就得根据提供的注解来处理这些表名和字段名的映射了。
最常见的注解如下:
- @TableName:用来指定表名
- @TableId:用来指定表中的主键字段信息
- @TableField:用来指定表中的普通字段信息
接下来我们详细讲解一下常见的注解。
TableName
用来映射数据库的表名。
上面的例子中我们可以看到,为什么MP会准确找到user这张表进行查询?是不是就是因为我们创建的实体类,名字就叫User,所以可以和数据库中的user表对应起来的关系。
那假如现在我的实例类名称换成Student,测试一下刚才写的根据id查用户的方法,看下什么效果:
Caused by: java.sql.SQLSyntaxErrorException: Table 'demo.student' doesn't exist
那么有时候我的实体类名称不一定和数据库中的表名能完全对应上,这时候@TableName注解就派上用场了。
我们可以在Student实体类加上这个注解:
@TableName("user")
@Data
public class Student {
再查就没有问题了。
@TableField
@TableName注解是把实体类和表做映射,那么表里面还有字段信息,类属性和表字段肯定也是一一映射的。
如果类属性和表字段无法一一对应时,假如我将 ”username“ 这个属性修改为 ”name“,调用查询方法就会报错:
Caused by: java.sql.SQLSyntaxErrorException: Unknown column 'name' in 'field list'
就需要@TableField注解的 value 属性即可解决。
@TableField("username")private String name;
除了value属性之外,还有两个我们需要关注的属性。
- exist
当我们的实体类中定义了一些数据库中不存在的字段时:
private String nickname;
调用查询接口就会报错:
Caused by: java.sql.SQLSyntaxErrorException: Unknown column 'nickname' in 'field list'
这种情况只需要设置一下exist属性即可解决:
@TableField(exist = false)private String nickname;
- select
该属性表示是否查询该字段。
比如我们给balance字段设置了select=false:
@TableField(select = false)private Integer balance;
我们调用查询语句就会发现SQL语句中就不再查询balance这个字段了。
==> Preparing: SELECT id,username AS name,password,phone,info,status,create_time,insert_time FROM user WHERE id=?
==> Parameters: 1(Integer)
<== Columns: id, name, password, phone, info, status, create_time, insert_time
<== Row: 1, Jack, 123456, 13111111111, {"age":20,"gender":"male"}, 1, 2025-02-19 14:33:54, 2025-02-19 14:33:54
<== Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@11f9535b]
Student(id=1, name=Jack, nickname=null, password=123456, phone=13111111111, info={"age":20,"gender":"male"}, status=1, balance=null, createTime=2025-02-19T14:33:54, insertTime=2025-02-19T14:33:54)
- fill
表示是否自动填充。参考官网
在我们日常的开发工作中,数据库表里面通常都会有create_time、update_time字段,而这两个字段通常都是取当前时间来插入,我们就需要使用到自动填充。
@TableField(fill = FieldFill.INSERT)private LocalDateTime createTime;@TableField(fill = FieldFill.INSERT_UPDATE)private LocalDateTime insertTime;
FieldFill是个枚举类,有下面几种类型:
- DEFAULT:不填充
- INSERT:在插入的时候填充
- UPDATE:在更新的时候填充
- INSERT_UPDATE:在插入或者更新时填充
做完这些,程序还不会帮我们自动去填充这两个字段,我们需要创建一个处理器:
package com.example.javawebdemo.handler;import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;import java.time.LocalDateTime;@Component
public class MyMetaObjectHandler implements MetaObjectHandler {@Overridepublic void insertFill(MetaObject metaObject) {// 此处的metaObject代表当前对象this.setFieldValByName("createTime", LocalDateTime.now(), metaObject);this.setFieldValByName("insertTime", LocalDateTime.now(), metaObject);}@Overridepublic void updateFill(MetaObject metaObject) {this.setFieldValByName("insertTime", LocalDateTime.now(), metaObject);}
}
我们再来调用保存用户的方法进行测试:
@Testpublic void insertUser() {Student student = new Student();student.setId(2L);student.setName("Rose");student.setPassword("123456");student.setPhone("13222222222");student.setInfo("{\"age\":18,\"gender\":\"female\"}");student.setStatus(1);student.setBalance(2000);
// student.setCreateTime(LocalDateTime.now());
// student.setInsertTime(LocalDateTime.now());userMapper.insert(student);}
可以看到两个时间被插入进来了:
==> Preparing: INSERT INTO user ( id, username, password, phone, info, status, balance, create_time, insert_time ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ? )
==> Parameters: 2(Long), Rose(String), 123456(String), 13222222222(String), {"age":18,"gender":"female"}(String), 1(Integer), 2000(Integer), 2025-02-19T15:33:55.894(LocalDateTime), 2025-02-19T15:33:55.894(LocalDateTime)
@TableId
@TableField注解是用来处理数据库中普通字段的,@TableId是用于处理主键字段的。
这注解中有几个属性需要重点关注一下:
- value
映射主键id的字段名,这个和TableField中的value类似。
- type
设置主键类型,主键的生成策略。这个type的值是使用一组枚举类型来表示的,它一共有五种类型:
类型 | 描述 |
---|---|
AUTO | 数据库自增 |
NONE(默认) | MP来设置主键,使用雪花算法实现 |
INPUT | 需要自己给主键赋值 |
ASSIGN_ID | MP使用Long、Integer、String来分配ID |
ASSIGN_UUID | MP分配UUID |
- NONE
我们的id字段是自增主键,但是我们上面测试插入数据的方法的时候,依然手动指定了,如果我们不指定默认情况就会是雪花算法实现的主键:
@Testpublic void insertUser() {Student student = new Student();student.setName("Lucy");student.setPassword("123456");student.setPhone("13333333333");student.setInfo("{\"age\":18,\"gender\":\"female\"}");student.setStatus(1);student.setBalance(2000);userMapper.insert(student);}
我们看一下这个主键id是1892117103913984001是雪花算法生产的数字:
==> Preparing: INSERT INTO user ( id, username, password, phone, info, status, balance, create_time, insert_time ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ? )
==> Parameters: 1892117103913984001(Long), Lucy(String), 123456(String), 13333333333(String), {"age":18,"gender":"female"}(String), 1(Integer), 2000(Integer), 2025-02-19T15:40:46.071(LocalDateTime), 2025-02-19T15:40:46.071(LocalDateTime)
- INPUT
这种类型比较简单,就是你手工在程序里给主键赋值。
- AUTO
这种方式就是交给数据库去处理,该自增自增。开发者无需处理了(就算你在程序里面手工给主键赋值,也只能按照数据库的自增来,相当于赋值无用)。
@TableId(value = "id", type = IdType.AUTO)private Long id;
我们表中就是自增id,因此一般可以用这种类型。
- ASSIGN_ID 和 ASSIGN_UUID
当使用该属性,同样是MP采用雪花算法生成一个随机值,赋值给你类中id属性,并把该属性插入到数据库,和ASSIGN_UUID的区别在于,ASSIGN_UUID规定id必须是String类型,数据库字段必须是varchar类型,而且数据库原先的主键自增得去掉,否则也会报错,字符串类型是无法自增的。
快速入门-常用配置
MyBatisPlus的配置项继承了MyBatis原生配置和一些自己特有的配置。比如:
mybatis-plus:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case: true # 是否开启下划线和驼峰映射cache-enabled: false # 是否开启二级缓存global-config:db-config:id-type: autoupdate-strategy: not_null # 更新策略:只更新非空字段type-aliases-package: com.example.javawebdemo.entity # 别名扫描包mapper-locations: "classpath*:/mapper/**/*.xml" # Mapper.xml文件地址,默认值
其他配置参考官网:https://baomidou.com/reference/
- type-aliases-package
MyBatis-Plus 会自动为 com.example.javawebdemo.entity 包下的每个类创建一个别名。默认别名是类名的 首字母小写 形式。例如,包中有一个名为 User 的类,则 MyBatis 会自动为它生成别名 user。
配置别名后,你可以在 MyBatis 的 XML 映射文件中直接使用这些别名,而不需要全限定类名,例如:
<resultMap id="UserResultMap" type="user"><id property="id" column="id"/><result property="username" column="username"/><result property="password" column="password"/>
</resultMap>
在 type 属性中使用 user 别名,代替了 com.example.javawebdemo.entity.User,从而使 XML 文件更简洁,提升代码可读性和可维护性。
核心功能-条件构造器
上面的查询都是基于id进行的,有些查询场景可能比较复杂,MP就提供了条件构造器来解决这类问题。
MyBatisPlus支持各种复杂的where条件,可以满足日常开发的所有需求。
这里的这些方法中需要传入Wrapper就是条件构造器,下面的Wrapper的继承体系:
我们这里截了有些Wrapper使用方法的图:
QueryWrapper顾名思义是做查询的,select * from tb where xxx,父类解决了where后面的条件,QueryWrapper拓展了select功能:
UpdataWrapper拓展了update功能:
- 基于QueryWrapper的查询案例
- 查询出名字中带o的,存款大于等于1000元的人的id、username、info、balance字段
@Testpublic void testQueryWrapper(){// 1. 构建查询条件QueryWrapper<User> wrapper = new QueryWrapper<User>().select("id", "username", "info", "balance").like("username", "o").ge("balance", 1000);// 2. 查询List<User> users = userMapper.selectList(wrapper);for (User user : users) {System.out.println(user);}}
我们看一下查询日志:
==> Preparing: SELECT id,username,info,balance FROM user WHERE (username LIKE ? AND balance >= ?)
==> Parameters: %o%(String), 1000(Integer)
<== Columns: id, username, info, balance
<== Row: 2, Rose, {"age":18,"gender":"female"}, 2000
<== Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@44a085e5]
User(id=2, username=Rose, password=null, phone=null, info={"age":18,"gender":"female"}, status=null, balance=2000, createTime=null, insertTime=null)
- 更新用户名为Jack的用户的余额为2000
@Testpublic void testQueryWrapper() {// 1. 要更新的数据User user = new User();user.setBalance(2000);// 2. 更新的条件QueryWrapper<User> wrapper = new QueryWrapper<User>().eq("username", "Jack");// 3. 执行更新userMapper.update(user, wrapper);}
我们看看执行过程日志:
==> Preparing: UPDATE user SET balance=?, insert_time=? WHERE (username = ?)
==> Parameters: 2000(Integer), 2025-02-19T16:50:01.476(LocalDateTime), Jack(String)
- 基于UpdateWrapper的更新
- 更新id为1,2的用户的余额,扣200
@Testpublic void testUpdateWrapper() {List<Integer> list = Arrays.asList(1, 2);UpdateWrapper<User> wrapper = new UpdateWrapper<User>().setSql("balance = balance - 200").in("id", list);userMapper.update(wrapper);}
查看执行过程:
==> Preparing: UPDATE user SET balance = balance - 200 WHERE (id IN (?,?))
==> Parameters: 1(Integer), 2(Integer)
我们这里再演示一下LambdaQueryWrapper的用法,其实和QueryWrapper是类似的,区别在于它在构造条件时是基于lambda语法,这样就可以在选择字段的时候不用硬编码了,我们现在都是把字段名称写死了:
@Testpublic void testLambdaQueryWrapper() {// 1. 构建查询条件LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>().select(User::getId, User::getUsername, User::getInfo, User::getBalance).like(User::getUsername, "o").ge(User::getBalance, 1000);// 2. 查询List<User> users = userMapper.selectList(wrapper);for (User user : users) {System.out.println(user);}}
- 条件构造器的用法:
- QueryWrapper和LambdaQueryWrapper通常用来构建select、delete、update的where条件部分
- UpdateWrapper和LambdaUpdateWrapper通常只有在set语句比较特殊才使用
- 尽量使用LambdaQueryWrapper和LambdaUpdateWrapper,避免硬编码
核心功能-自定义SQL
我们可以利用MyBatisPlus的Wrapper来构建复杂的Where条件,然后自己定义SQL语句中剩下的部分。
我们用案例来体验一下:
- 需求:将id在指定范围的用户(例如1、2)的余额扣减指定值
这个需求我们上面就做过:
@Testpublic void testUpdateWrapper() {List<Integer> list = Arrays.asList(1, 2);UpdateWrapper<User> wrapper = new UpdateWrapper<User>().setSql("balance = balance - 200").in("id", list);userMapper.update(wrapper);}
上面的代码有什么问题呢?我们现在写的逻辑是业务逻辑,将来是在service层定义,这就相当于我们把一部分sql语句写到service层了,这在很多企业开发规范中是不允许的,他们会要求只能在mapper和mapper.xml中定义SQL语句,所以上面的写法就得改造了。
既然MP擅长where条件的构建,一旦出现复杂场景,比如:
那就把where条件交给MP做,剩下的自定义。把MP构建好的where条件传递给mapper层,在mapper层或者mapper.xml中实现SQL组装,那具体怎么实现传递和组装呢?需要三步:
- 基于Wrapper构建where条件
- 在mapper方法参数中用Param注解声明wrapper变量名称,必须是ew
- 自定义SQL,并使用Wrapper条件
我们来跟着这三个步骤实现一下:
(1)基于Wrapper构建where条件
@Testpublic void testCustomSqlUpdate() {// 更新条件List<Integer> ids = Arrays.asList(1, 2);int amount = 200;// 定义条件LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>().in(User::getId, ids);// 自定义SQLuserMapper.updateBalanceByIds(wrapper,amount);}
(2)在mapper方法参数中用Param注解声明wrapper变量名称,必须是ew
package com.example.javawebdemo.mapper;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.example.javawebdemo.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;@Mapper
public interface UserMapper extends BaseMapper<User> {void updateBalanceByIds(@Param(Constants.WRAPPER) LambdaQueryWrapper<User> wrapper, @Param("amount") int amount);}
(3)自定义SQL,并使用Wrapper条件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.javawebdemo.mapper.UserMapper"><update id="updateBalanceByIds">UPDATE user SET balance = balance - #{amount} $(ew.customSqlSegment)</update>
</mapper>
接下来我们测试一下:
==> Preparing: UPDATE user SET balance = balance - ? WHERE (id IN (?,?))
==> Parameters: 200(Integer), 1(Integer), 2(Integer)
核心功能-IService接口基本用法
MP还提供了IServcie接口,只要继承了它,连Service代码都可以不用写了。
下面我们看看IService提供的一些方法:
可以看到把常用的增删改查都封装好了。
使用IService之前,看看我们之前开发流程:
需要自定义Service接口,并写一个实现类。
现在IService中有大量封装好的方法,我们只要继承它就可以,现在的继承体系如下:
总结下来MP的Service接口使用流程就是两步:
- 自定义Service接口继承IService接口
- 自定义Service实现类,实现自定义接口并继承ServiceImpl类
接下来我们按照这两步骤,代码中写一下:
(1)自定义Service接口继承IService接口
package com.example.javawebdemo.service;import com.baomidou.mybatisplus.extension.service.IService;
import com.example.javawebdemo.entity.User;public interface IUserService extends IService<User> {
}
(2)自定义Service实现类,实现自定义接口并继承ServiceImpl类
package com.example.javawebdemo.service.impl;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.javawebdemo.entity.User;
import com.example.javawebdemo.mapper.UserMapper;
import com.example.javawebdemo.service.IUserService;
import org.springframework.stereotype.Service;@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
}
然后我们写逻辑测试一下:
- 新增用户
@Autowiredprivate IUserService userService;@Testpublic void testSaveUser(){User user = new User();user.setUsername("Curry");user.setPassword("123456");user.setPhone("18888888888");user.setInfo("{\"age\":30,\"gender\":\"male\"}");user.setStatus(1);user.setBalance(20000000);userService.save(user);}
执行日志可以看到插入成功了:
==> Preparing: INSERT INTO user ( username, password, phone, info, status, balance, create_time, insert_time ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ? )
==> Parameters: Curry(String), 123456(String), 18888888888(String), {"age":30,"gender":"male"}(String), 1(Integer), 20000000(Integer), 2025-02-20T09:54:39.429(LocalDateTime), 2025-02-20T09:54:39.429(LocalDateTime)
这里有个问题,就是我们在配置文件中配了全局id自增策略,由于刚开始默认是雪花的,所以后面就算是已经设置了自增,也是基于之前ID数,自增了。
- 根据id查询
@Testpublic void testQuery(){List<User> users = userService.listByIds(Arrays.asList(1, 2));for (User user : users) {System.out.println(user);}}
执行日志:
==> Preparing: SELECT id,username,password,phone,info,status,balance,create_time,insert_time FROM user WHERE id IN ( ? , ? )
==> Parameters: 1(Integer), 2(Integer)
<== Columns: id, username, password, phone, info, status, balance, create_time, insert_time
<== Row: 1, Jack, 123456, 13111111111, {"age":20,"gender":"male"}, 1, 1600, 2025-02-19 14:33:54, 2025-02-19 16:54:06
<== Row: 2, Rose, 123456, 13222222222, {"age":18,"gender":"female"}, 1, 1600, 2025-02-19 15:33:56, 2025-02-19 15:33:56
<== Total: 2
核心功能-IService开发基础业务接口
我们发现MP提供的BaseMapper和IService中的方法很多是重复的,那实际业务开发中,到底该使用哪个接口提供的方法呢?接下来用几个案例来看看实际开发如何使用。
- 基于Restful风格实现下列接口
这个不是像上面做单元测试,而是写接口,做完成的业务开发。在实现之前要做点准备工作:
(1)引入相关依赖
- 引入Swagger依赖,方便做单元测试
- 引入Web依赖,编写Swagger相关功能
<!-- swagger--><dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-openapi2-spring-boot-starter</artifactId><version>4.1.0</version></dependency><!-- web--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- hutool用到BeanUtil等工具类--><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.21</version></dependency>
(2)swagger相关配置
knife4j:enable: trueopenapi:title: 用户接口管理文档description: "用户接口管理文档"email: xuec_7@163.comconcat: sunnyurl: https://sunnyrivers.blog.csdn.net/version: v1.0.0group:default:group-name: defaultapi-rule: packageapi-rule-resources:- com.example.javawebdemo.controller
(3)实体类
我们把所有实体相关类都放在domain包:
package com.example.javawebdemo.domain.dto;import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;@Data
@ApiModel(description = "用户表实体")
public class UserFromDTO {@ApiModelProperty("id")private Long id;@ApiModelProperty("用户名")private String username;@ApiModelProperty("密码")private String password;@ApiModelProperty("注册手机号")private String phone;@ApiModelProperty("详细信息,JSON风格")private String info;@ApiModelProperty("账户余额")private Integer balance;
}
新增用户接口开发:
package com.example.javawebdemo.controller;import cn.hutool.core.bean.BeanUtil;
import com.example.javawebdemo.domain.dto.UserFromDTO;
import com.example.javawebdemo.domain.entity.User;
import com.example.javawebdemo.service.IUserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@Api(tags = "用户管理接口")
@RequestMapping("/users")
@RestController
@RequiredArgsConstructor
public class UserController {// @Autowired Spring不推荐用这种方法注入: Field injection is not recommended ,推荐用构造函数// 构造函数可以用lombok简写,但是用哪个构造注解呢?一个类的成员变量有很多,但并不是所有的都需要注入// 首先给类加上final,也就是必须在初始化的时候,给变量初始化,// 然后用@RequiredArgsConstructor注解,它只会对一些需要一开始初始化的变量构造private final IUserService userService;@ApiOperation("新增用户接口")@PostMappingpublic void saveUser(@RequestBody UserFromDTO userDTO) {// 以前我们都是先写service方法,然后再写mapper方法,现在我们有了MP后,我们写接口的时候,// 我们先考虑这个功能MP是否已经提供了?// 1.把DTO拷贝到ENTITYUser user = BeanUtil.copyProperties(userDTO, User.class);// 2. 保存userService.save(user);}
}
我们一行service代码都没有编写,一个新增用户的接口就开发完成了。
同理删除用户接口:
@ApiOperation("删除用户接口")@DeleteMapping("{id}")public void deleteUserById(@ApiParam("用户id") @PathVariable("id") Long id) {userService.removeById(id);}
根据id查询用户接口:
@ApiOperation("根据id查询用户接口")@GetMapping("{id}")public UserVO queryUserById(@ApiParam("用户id") @PathVariable("id") Long id) {User user = userService.getById(id);// 把entity拷贝到VOreturn BeanUtil.copyProperties(user, UserVO.class);}
根据id批量查询接口:
@ApiOperation("根据id批量查询用户接口")@GetMapping()public List<UserVO> queryUserByIds(@ApiParam("用户id集合") @RequestParam("ids") List<Long> ids) {List<User> users = userService.listByIds(ids);return BeanUtil.copyToList(users, UserVO.class);}
上面的接口都比较简单,没什么业务逻辑,都直接在controller里面都搞定了,有些接口有复杂的业务逻辑,就需要自定义service,当然还有些复杂的,我们还要在mapper中自定sql语句。
根据id扣减余额接口:
这个接口需要写业务逻辑,比如拿到用户的id,先要判断状态是否正常,只有正常才扣减,还要判断余额是否充足,只有充足才能扣减,所以这里面有很多业务判断,而MP中的方法是没有业务逻辑的。
还有我们更新余额的sql语句,不建议在业务层来写,需要自定义SQL语句完成更新。
controller层:
@ApiOperation("扣减余额接口")@PutMapping("/{id}/deduction/{money}")public void deductBalance(@ApiParam("用户id") @PathVariable("id") Long id,@ApiParam("扣减的金额") @PathVariable("money") Integer money) {userService.deductBalance(id, money);}
service层:
package com.example.javawebdemo.service.impl;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.javawebdemo.domain.entity.User;
import com.example.javawebdemo.mapper.UserMapper;
import com.example.javawebdemo.service.IUserService;
import org.springframework.stereotype.Service;@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {@Overridepublic void deductBalance(Long id, Integer money) {// 查询用户User user = getById(id);// 校验用户状态if (user == null || user.getStatus() == 2) {throw new RuntimeException("用户状态异常");}// 检验余额是否充足if (user.getBalance() < money) {throw new RuntimeException("用户余额不足");}// 扣减余额baseMapper.deductBalance(id, money);}
}
mapper层:
package com.example.javawebdemo.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.javawebdemo.domain.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;@Mapper
public interface UserMapper extends BaseMapper<User> {void deductBalance(@Param("id") Long id, @Param("money") Integer money);
}
mapper.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.javawebdemo.mapper.UserMapper"><update id="deductBalance">UPDATE user SET balance = balance - #{money}<where>id = #{id}</where></update>
</mapper>
写完代码,我们启动服务进行测试,先打开swagger:
http://localhost:8080/doc.html
接下来就可以在swagger中进行接口测试了。
其他接口可以进行测试,这里就不演示了。
核心功能-IService提供的lambda方法
我们依然是通过案例来讲解
- 需求一:实现一个根据复杂条件查询用户的接口,查询条件如下:
(1)name:用户名关键字,可以为空
(2)status:用户状态,可以为空
(3)minBalance:最小余额,可以为空
(4)maxBalance:最大余额,可以为空
这个需求如果我们按照之前mybatis的写法将会是这样的:
<select id="queryUsers" resultType="com.example.javawebdemo.domain.entity.User">SELECT * FROM user<where><if test="name != null">AND username LIKE #{name}</if><if test="status != null">AND status = #{status}</if><if test="minBalance != null and maxBalance != null">AND balance between #{minBalance} AND #{maxBalance}</if></where></select>
可以看到这种写法非常麻烦,我们看看IService的Lambda是如何做的。
这里写的方法中参数可以用@RequestParam(value = “xxx”,required = false)来注解,但是如果参数很多就推荐用定义一个对象来接收:
package com.example.javawebdemo.domain.query;import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;@Data
@ApiModel(description = "用户查询条件实体")
public class UserQuery {@ApiModelProperty("用户名关键字")private String name;@ApiModelProperty("用户状态:1-正常,2-冻结")private Integer status;@ApiModelProperty("余额最小值")private Integer minBalance;@ApiModelProperty("余额最大值")private Integer maxBalance;
}
controller层:
@ApiOperation("根据复杂条件查询用户接口")@GetMapping("/list")public List<UserVO> queryUser(UserQuery query) {List<User> users = userService.queryUser(query.getName(), query.getStatus(), query.getMinBalance(), query.getMaxBalance());return BeanUtil.copyToList(users, UserVO.class);}
service层:
@Overridepublic List<User> queryUser(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();}
重启服务进行测试:
我们查询名字中带o的用户:
除了查询外还提供了一个更新的方法,也很方便,我们用案例讲解。
- 需求:改造根据id修改用户余额的接口,要求如下
(1)完成对用户状态校验
(2)完成对用户余额校验
(3)如果扣减后余额为0,则将用户status修改为冻结状态-2
controller层:
@ApiOperation("扣减余额接口")@PutMapping("/{id}/deduction/{money}")public void deductBalance(@ApiParam("用户id") @PathVariable("id") Long id,@ApiParam("扣减的金额") @PathVariable("money") Integer money) {userService.deductBalance(id, money);}
service层:
@Transactionalpublic void deductBalance(Long id, Integer money) {// 查询用户User user = getById(id);// 校验用户状态if (user == null || user.getStatus() == 2) {throw new RuntimeException("用户状态异常");}// 检验余额是否充足if (user.getBalance() < money) {throw new RuntimeException("用户余额不足");}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();}
重启服务进行测试:
Lucy余额原来的1800,扣减1800后余额为0,状态也改成2了:
- 需求3:批量插入10万条用户数据,并作出对比:
(1)普通for循环插入
(2)IService的批量插入
(3)开启rewriteBatchedStatements=true参数
我们先写个方法,给数据库中插入10W条数据,用普通for循环插入:
private User buildUser(int i) {User user = new User();user.setUsername("user_" + i);user.setPassword("123456");user.setPhone("" + (18688190000L + i));user.setInfo("{\"age\":30,\"gender\":\"male\"}");user.setBalance(2000);return user;}@Testvoid testSaveOneByOne() {long b = System.currentTimeMillis();for (int i = 0; i < 100000; i++) {userService.save(buildUser(i));}long e = System.currentTimeMillis();System.out.println("耗时:" + (e - b));}
接下来我们把数据清空,接下来我们测试一下批处理:
@Testvoid testSaveBatch() {// 每次批量插入1000条,插入100次List<User> list = new ArrayList<>(1000);long b = System.currentTimeMillis();for (int i = 0; i < 100000; i++) {list.add(buildUser(i));if (i % 1000 == 0) {userService.saveBatch(list);list.clear();}}long e = System.currentTimeMillis();System.out.println("耗时:" + (e - b));}
可以看到效率提升还是比较明显,核心就是减少网络请求的次数。如果把sql语句变成类似于:
那句只需要1次网络请求,效率会更快。
其实只要把mysql的参数rewriteBatchedStatements=true即可。
默认情况下,MySQL JDBC驱动(Connector/J)的addBatch()方法仅作语句缓存,实际执行时会逐条发送SQL语句到数据库,开启rewriteBatchedStatements=true后,驱动会重写批量语句为更高效的格式:
INSERT INTO table (col1,col2) VALUES (1,2),(3,4),(5,6) -- 合并为多值语句
UPDATE table SET col=val WHERE id=1; UPDATE table SET col=val WHERE id=2 -- 分号连接多语句
接下来我们重新执行批处理的代码,发现10W条数据大约用了7s就插入完成了。
总结一下批处理的三种方案:
扩展功能-静态工具
静态工具提供的方法和IService方法基本上一样,只不过IService方法中的接口都是非静态的,而Db静态工具里面的方法都是静态的,我们用IService的时候就需要继承它,还要传入实体类,这样IService就能通过反射得到实体类的字节码,得到表信息从而实现增删改查;而静态工具的每个方法都需要传入实体类的字节码:
那既然两个功能相似为什么要搞两个呢?
开发业务的时候,有的时候会出现多个service相互调用的场景,如果采用传统的方式,也就是用@Autowired来注入,那Service之间相互注入就会出现循环依赖,因此此时就直接用静态工具。
扩展功能-逻辑删除
逻辑删除就是基于代码逻辑模拟删除效果,但并不会真正删除数据。思路如下:
- 在表中添加一个字段标记数据是否被删除
- 当删除数据时把标记置为1
- 查询时只查询标记为0的数据
例如逻辑删除字段为deleted:
-
删除操作:
-
查询操作:
MybatisPlus提供了逻辑删除功能,无需改变方法调用的方式,而是在底层帮我们自动修改CRUD的语句。我们要做的就是在application.yaml文件中配置逻辑删除的字段名称和值即可:
扩展功能-枚举处理器
枚举处理器可以实现java中的枚举类型和数据库之间的相互转换。
比如现在有下面User实体类,用户状态1代表正常、2代表冻结:
我们在比较或者赋值的时候都是基于数字进行的,如果类型有很多的情况下,那我们就不知道哪个数字对应什么状态了,这个时候我们就可以用枚举来表示:
那将来不管有多少枚举项,都可以在这写出来,这样一看到枚举对于每个数字代表什么状态就一目了然了。
现在状态字段是Integer类型,我们在用set赋值的时候依然得赋1和2,这样还存在两个问题:
- 每次比较或赋值的时候还得查一下枚举
- 代码的可读性很差
为了解决这个问题,这里的状态字段就不要再用Integer类型了,就应该用枚举类型:
private UserStatus status;
但是这样又有一个问题:我们数据表中status依然是int类型,这就存在java中的枚举类型和数据库之间相互转换的问题。其实不仅仅是枚举类型,java中的所有类型都需要和数据库类型进行相互转换,只不过底层都是由mybatis帮我们做了:
我们看到上面也有枚举类型,但是它的功能有限,仅支持枚举的序数(ordinal())或名称(name())与数据库字段的简单映射。例如,将枚举的ordinal()值(如0,1,2)存入数据库,功能较为基础:
public enum UserStatus {ENABLED, // ordinal = 0 DISABLED; // ordinal = 1
}
- 插入数据
当插入一个 User 对象时,若 status = UserStatus.ENABLED,MyBatis 会将其转换为 0 存入数据库。 - 查询数据
若数据库中的 status 值为 1,MyBatis 会将其转换为 UserStatus.DISABLED。 - 存在问题
如果调整枚举定义的顺序(例如将 DISABLED 定义在 ENABLED 之前),ordinal() 值会变化,导致历史数据错乱。
所以上面有两个是mybatisplus进行扩展的类:MybatisEnumTypeHandler和AbstractJsonTypeHandler。
如何实现PO类中的枚举类型变量与数据库字段的转换?
- 给枚举中的与数据库对应value值添加@EnumValue注解
- 在配置文件中配置统一的枚举处理器,实现类型转换
我们做个测试:
定义枚举类:
package com.example.javawebdemo.enums;import com.baomidou.mybatisplus.annotation.EnumValue;
import lombok.Getter;@Getter
public enum UserStatus {NORMAL(1, "正常"),FROZEN(2, "冻结"),;@EnumValueprivate final int value;private final String desc;UserStatus(int value, String desc) {this.value = value;this.desc = desc;}
}
配置文件:
mybatis-plus:configuration:default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
用枚举类替换Integer:
package com.example.javawebdemo.domain.entity;import com.baomidou.mybatisplus.annotation.*;
import com.example.javawebdemo.enums.UserStatus;
import lombok.Data;import java.time.LocalDateTime;@TableName("user")
@Data
public class User {private Long id;private String username;private String password;private String phone;private String info;private UserStatus status;private Integer balance;@TableField(fill = FieldFill.INSERT)private LocalDateTime createTime;@TableField(fill = FieldFill.INSERT_UPDATE)private LocalDateTime insertTime;
}
插件功能
MyBatisPlus提供的内置拦截器有下面这些:
我们这里主要讲解分页插件的使用:
- 注册一个配置类,并编写paginationInterceptor方法
@Configuration
public class MybatisPlusConfig {@Beanpublic PaginationInterceptor paginationInterceptor() {return new PaginationInterceptor();}}
- 编写分页查询代码
@Test
void select() {// Page构造方法有两个参数:current-当前页;size:-页大小Page<User> page = new Page<>(1, 10);// 返回值也是一个Page类型,里面包含了分页信息以及查询出来的数据信息。Page<User> result = mapper.selectPage(page, null);// 查询的结果在Records中result.getRecords().forEach(System.out::println);
}
参考文献
https://baomidou.com/guides/mybatis-x/
https://blog.csdn.net/qq_41320700/article/details/143837356