MyBatis-Plus是一个MyBatis的增强工具,在MyBatis的基础上只做增强不做改变,为简化开发、提高效率而生。
MyBatis-Plus和MyBatis是共生的,而非替代品。MyBatis-Plus只做增强不做改变,引入它不会对现有工程产生影响。其会在启动时自动注入基本CRUD(增删改查),性能基本无损耗,直接面向对象操作,使得开发人员能够更加方便地进行数据库操作。
快速入门
我们基于之前《案例》中创建的数据表进行快速入门的学习。
一、引入MyBatis-Plus的依赖:
XML
<!--MyBatis-Plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.7</version>
</dependency>
此时原本的mybatis便已不再需要,可以删掉。
二、使Mapper层的接口继承自接口BaseMapper<T>,泛型为对应的实体类:
java
public interface EmpMapper extends BaseMapper<Emp> {}
public interface DeptMapper extends BaseMapper<Dept> {}
该接口会提供大量的增删改查(CRUD)方法,且命名规范都是insert、delete、select、update开头。此时我们就可以使用MyBatis-Plus提供的方法,且之前定义的方法也仍然可以使用(方法名不重复的前提下)。但为了避免相似代码不变理解,建议删除。
此时MyBatis-Plus的基础使用便已完成。
注解
上文说过继承BaseMapper需要指定泛型,MB即可基于反射得到指定的实体类的类型,并利用反射得到对应的字节码以获取实体类信息,接下来就可以把实体类信息作为数据库/表信息了。
而实体类和数据表中的信息的对应关系按照指定规则来实现:
- 类名驼峰转下划线作为表名
- 名为id的字段作为主键
- 属性名驼峰转下划线作为字段名
如果不符合上述规则,则需通过注解自行定义表名和字段名。
一、@TableName:用于指定实体类对应的数据库表名。当实体类的类名与数据库表名不一致时,可以使用这个注解来指定表名。
java
//数据表名为tb_user
@TableName("tb_user")
public class User {
// ...
}
二、@TableId:用于指定实体类中作为主键的字段。这个注解可以配置主键的类型,比如自增主键、UUID等。
java
public class User {
//数据表User中主键为tb_id
@TableId(value="tb_id",ftype = IdType.AUTO)
private Long id;
// ...
}
其中"value="可省略,IdType.AUTO 表示主键自增,其他的例如IdType.INPUT:需要手动输入主键值,插入数据时需要自己指定主键的值、IdType.UUID:自动生成 UUID 作为主键值、IdType.AUTO:数据库中该字段需要支持自增
三、@TableField:用于指定实体类中字段与数据库表字段的映射关系。他有很多使用场景:
- 成员变量名与数据字段名不一致
- 在字段上加@TableField("user_name")即可
- 成员变量名以is开头,且是布尔值
- MB在底层会基于反射类型的机制获取字段名称,is开头的变量会将is去掉作为变量名,需加上@TableField("is_active")
- 成员变量名与数据库关键字冲突
- 例如order之类与数据关键字冲突的字段,需将其作为转义字符@TableField("'order'")
- 成员变量不是数据库字段
- 该属性在数据表中无对应的字段,不希望其参与到相关方法则需加上@TableField(exist = false)
java
public class User {
// 成员变量名与数据库字段名不一致的情况
@TableField("real_name")
private String name; // 数据库字段名为 real_name
// 成员变量名以 is 开头,且是布尔值的情况
@TableField("is_active")
private Boolean active; // 数据库字段名为 is_active
// 成员变量名与数据库关键字冲突的情况
@TableField("'order'")
private Long order; // 数据库字段名为 user_id
// 成员变量不是数据库字段的情况
@TableField(exist = false)
private String extraInfo; // 这个成员变量不会被持久化到数据库中
}
配置
MyBatis-Plus的配置项继承了MyBatis原生配置和一些自己特有的配置:
XML
#MyBatis-Plus
mybatis-plus:
type-aliases-package: org.example.mybatistest.pojo # 指定别名扫描包,即实体类所在的包
mapper-locations: classpath*:/mapper/**/*.xml # 指定Mapper XML文件的路径,支持通配符匹配多个目录下的文件
configuration:
map-underscore-to-camel-case: true # 开启驼峰命名自动映射开关
cache-enabled: false # 禁用二级缓存
global-config: #全局配置
db-config:
id-type: AUTO # 主键类型
update-strategy: NOT_NULL # 更新策略
这些配置大多使用其默认值即可,无需单独配置,因此我们熟悉就好,实际运用到了可以到官方配置文档查阅:
使用配置 | MyBatis-Plushttps://baomidou.com/reference/
核心功能
条件构造器
之前演示的方法比如"根据ID查找对应数据"都比较简单,但实际使用中我们可能遇到复杂的查询,MB提供的条件构造器可以解决这一问题。
方法名称大致相同,但参数为Wrapper类的变量,这个Wrapper类为我们提供了一种灵活且强大的方式来构建复杂的查询条件。实现方法分三步:
- 创建Wrapper实例:首先,我们需要创建一个QueryWrapper<UserList>的实例。这里的UserList是我们定义的实体类。
- 构建限制条件:调用实例的方法来实现对应的sql语句,如果语句较复杂可使用.setSql()方法
- 执行:将构建好的Wrapper实例传递给userMapper的方法作为参数并执行。
我们以例子来演示:将sql语句转为同功能的语句:
sql
# 查询名字中包含o且id>10的数据------------------------------------------------------------------------------------------
select id, username, password, gender, job
from user_list
where username like '%o%' and id>10;
# 将用户名为yangxiao,密码为123456的数据的job修改为3------------------------------
update user_list
set job=3
where username = 'yangxiao'
and password = '123456';
#将id为1,2,4的数据的创建时间增加一个月---------------------------------------------------------------------
<update id="updateCreateTime">
update user_list
set creat_time= date_add(create_time, interval 1 month)
where id in
<foreach collection="ids" separator="," item="id" open="(" close=")">
#{ids}
</foreach>
</update>
java
@Test//------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
void testSelect() {
//构建查询条件
QueryWrapper<UserList> myWrapper = new QueryWrapper<UserList>()
.select("id", "username", "password", "gender", "job") // 添加所有需要的字段
.like("username", "%o%") // 修改like方法以匹配SQL查询
.ge("id", 10);
//查询
List<UserList> myuser = userMapper.selectList(myWrapper);
// 输出查询结果
for (UserList user : myuser) {
System.out.println(user);
}
}
@Test//------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
void updateByAandP() {
UpdateWrapper<UserList> updateWrapper = new UpdateWrapper<UserList>()
.eq("username", "yangxiao")
.eq("password", "123456")
.set("job", 3); // 设置 job 字段的新值
}
@Test//------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
void updateT() {
// 创建 查询条件
List<Long> ids=List.of(1L,2L,4L);
UpdateWrapper<UserList> updateWrapper = new UpdateWrapper<UserList>()
.setSql("create_time = DATE_ADD(create_time, INTERVAL 1 MONTH)")
.in("id", ids);
// 执行更新操作
userMapper.update(null,updateWrapper);
}
上文的编程我们都是使用硬编码的写法,我们可使用lombdaWrapper来解决这一问题:
java
@Test
void updateByAandP() {
LambdaUpdateWrapper<UserList> updateWrapper = new LambdaUpdateWrapper<UserList>()
.eq(UserList::getUsername, "yangxiao")
.eq(UserList::getPassword, "123456")
.set(UserList::getJob, 3); // 设置 job 字段的新值
// 执行更新操作
int updatedRows = userMapper.update(null, updateWrapper);
}
自定义SQL
尽管条件构造器功能强大,但在某些复杂场景下,你可能需要编写自定义 SQL 来实现特定的查询逻辑。我们可以利用MB的Wrapper来构建复杂的Where语句,然后自己定义SQL语句中剩下的部分。
以例子"将id为1,2,4的工资增加200"为例,我们在其基础上实现自定义SQL。利用MyBatisPlus的Wrapper来构建复杂的Where条件,然后自己定义SQL语句中剩下的部分。
一、基于Wrapper构建where条件
java
List ids = List.of(1L,2L,4L);
int amount = 200;
//1.构建条件
LambdaQueryWrapper wrapper = new LambdaQueryWrapper().in(User::getId, ids);
//2.自定义SQL方法调用
userMapper.updateBalanceByIds(wrapper,amount);
二、在mapper方法参数中用Param注解声明wrapper变量名称,必须是ew
java
void updateBalanceByIds(@Param("ew") LambdaQueryWrapper wrapper,@Param("amount") int amount);
三、自定义SQL,并使用Wrapper条件
sql
UPDATE tb_user SET balance = balance - #{amount} ${ew.customSqlSegment}
Service接口
上文都是在Mapper层继承BaseMapper,同理我们还可以使Service层继承IService接口,其大体也离不开增删改查。同时因为继承该接口需要实现其方法,MB还提供了对应的实现类ServiceImpl,我们可以让自定义的接口继承自IService,自定义的实现类继承自ServiceImpl。
java
//接口
public interface chnService extends IService<UserList> {
}
//实现类
@Service//需指定泛型,第一个为对应的Mapper接口,第二个为对应的实体类
public class chnServiceImpl extends ServiceImpl<UserMapper, UserList> implements chnService {
}
以其为基础开发controller层:
java
@RestController
@RequestMapping("/user")
public class chnController {
@Autowired
private chnService chnservice;
//新增用户
@PostMapping
public void saveUser(@RequestBody UserList targetUser) {//@RequestBody接收json类参数
chnservice.save(targetUser);
}
//根据ID删除用户
@DeleteMapping("{id}")
public void deleteById(@PathVariable("id") Long id) {//@RequestBody接收json类参数
chnservice.removeById(id);
}
//根据ID查询用户
@GetMapping("{id}")
public UserList queryById(@PathVariable("id") Long id) {//@RequestBody接收json类参数
return (UserList)chnservice.getById(id);
}
//根据ID查询多个用户
@GetMapping
public List<UserList> queryByIds(@PathVariable("ids")List<Long> ids) {//@RequestBody接收json类参数
return (List<UserList>)chnservice.listByIds(ids);
}
}
以上都是一些较为简单的方法,我们接下来演示复杂的业务方法:
java
//Controller------------------------------------------------------------------------------------------
//扣除用户余额
@PutMapping("/{id}/deduction/{money}")
public void delMoneyById(@PathVariable("id") Long id,@PathVariable("money") Integer money) {//@RequestBody接收json类参数
chnservice.delMoneyById(id,money);
}
//Service------------------------------------------------------------------------------------------------
void delMoneyById(Long id, Integer money);
//ServiceImpl------------------------------------------------------------------------------------------------
@Override
public void delMoneyById(Long id, Integer money) {
//查询用户
UserList targetUser= this.getById(id);
//调用自身的getById()方法,this.可省略
//校验用户状态和余额
if(targetUser==null||targetUser.getStatus()==2)
throw new RuntimeException("用户状态异常!");
if(targetUser.getBal()<money)
throw new RuntimeException("用户余额不足!");
//扣除金额
baseMapper.delMoneyById(id,money);
}
//UserMapper------------------------------------------------------------------------------------------
@Update("update user_list set bal=bal-#{money} where id=#{id}")
void delMoneyById(@Param("id")Long id,@Param("money") Integer money);
在IService也提供了一些有关Lambda的方法,我们仍通过几个案例来查看:
需求:实现一个根据复杂条件查询用户的接口,查询条件如下:
- name: 用户名关键字,可以为空
- status: 用户状态,可以为空
- minBalance: 最小余额,可以为空
- maxBalance: 最大余额,可以为空
因为查询条件较多,且都有可能为空,因此我们新创建一个类UserQuery来接收参数:
java
public class UserQuery {//略
//Controller------------------------------------------------------------------------------------------
//复杂条件查询
@GetMapping("/list")
public List<UserList> queryUsers(UserQuery query) {
List<UserList>users=chnService.queryUsers(query.getName(),query.getStatus(),query.getMin(),query.getMax());
}
//Service------------------------------------------------------------------------------------------------
List<UserList> queryUsers(String name, Integer status, Integer min, Integer max);
//ServiceImpl------------------------------------------------------------------------------------------------
@Override
public List<UserList> queryUsers(String name, Integer status, Integer min, Integer max) {
// 使用lambdaQuery()开始构建查询条件
return lambdaQuery()
// 如果name不为null,则添加一个模糊查询条件,匹配用户名中包含name的记录
.like(name != null, UserList::getUsername, name)
// 如果status不为null,则添加一个等值查询条件,匹配状态等于status的记录
.eq(status != null, UserList::getStatus, status)
// 如果min不为null,则添加一个大于等于的查询条件,匹配最小值大于等于min的记录
.ge(min != null, UserList::getMin, min)
// 如果max不为null,则添加一个小于等于的查询条件,匹配最大值小于等于max的记录
.le(max != null, UserList::getMax, max)
// 执行查询并返回查询结果列表
.list();
}
再来看下一个案例:改造根据id修改用户余额的接口,要求
- 完成对用户状态校验
- 完成对用户余额校验
- 如果扣减后余额为0,则将用户status修改为2,意为冻结状态
前两步之前已完成,我们主要来看第三步
java
@Override
public void delMoneyById(Long id, Integer money) {
//查询用户
UserList targetUser= this.getById(id);
//调用自身的getById()方法,this.可省略
//校验用户状态和余额
if(targetUser==null||targetUser.getStatus()==2)
throw new RuntimeException("用户状态异常!");
if(targetUser.getBal()<money)
throw new RuntimeException("用户余额不足!");
//扣除金额
int newBal=targetUser.getBal()-money;
lambdaUpdate()
.set(UserList::getBal,newBal)
.set(newBal==0,UserList::getStatus,2)
.eq(targetUser::getId,id)
.eq(targetUser::getBal,targetUser.getBal())
.update();
}
再使用IService实现批量插入有两种方法:
java
@Test//方法一------------------------------------------------------------------------------
void testSaveOneByOne() {
// 记录开始时间
long b = System.currentTimeMillis();
// 循环插入10万条数据,每条数据单独保存
for (int i = 1; i <= 100000; i++) {
userService.save(buildUser(i));
}
// 记录结束时间
long e = System.currentTimeMillis();
// 打印出总耗时
System.out.println("耗时:" + (e - b));
}
@Test方法二------------------------------------------------------------------------------------
void testSaveBatch() {
// 批量插入数据,每次插入1000条,总共插入10万次
// 1. 创建一个初始容量为1000的列表,用于存储批量插入的数据
List<User> list = new ArrayList<>(1000);
// 记录开始时间
long b = System.currentTimeMillis();
for (int i = 1; i <= 100000; i++) {
// 2. 构建用户对象并添加到列表中
list.add(buildUser(i));
// 3. 当列表中的数据达到1000条时,执行批量保存操作
if (i % 1000 == 0) {
userService.saveBatch(list);
// 4. 批量保存后清空列表,为下一批数据做准备
list.clear();
}
}
// 记录结束时间
long e = System.currentTimeMillis();
// 打印出总耗时
System.out.println("耗时:" + (e - b));
}
第一种方法逐个保存用户信息。它首先记录当前时间戳作为开始时间 (b),然后在一个循环中依次创建用户对象并调用 userService.save() 方法进行保存。最后,再次获取当前时间戳作为结束时间 (e),并打印出整个过程所花费的时间。
这种方法的特点是简单直接,但效率较低,因为对于每个用户都进行了独立的数据库操作。
第二种方法采用批量保存的方式来提高效率。它同样先记录开始时间 (b),然后在循环中收集用户对象到一个列表 (list) 中。当列表中的用户数量达到一定阈值(例如1000个)时,就调用 userService.saveBatch() 方法进行批量保存,并将列表清空以继续收集下一个批次的数据。最后,记录结束时间 (e) 并打印出耗时。
第二种方法通过减少数据库交互次数显著提高了性能,尤其是在处理大量数据时更为明显。
但第二种方法在实际上仍是一个一个插入,这是因为MySQL有个属性rewriteBatchedStatements用于优化批处理操作的性能,默认为false,如果将其改为true,系统则会将1000条数据合并成一条SQL语句一起插入一次。
java
spring:
datasource:
jdbc-url: jdbc:mysql://host:port/database?rewriteBatchedStatements=true
#连接第一个参数用"?"后续再连接其他参数用"&"
扩展功能
代码生成
之前写的代码是对员工表进行操作,后续我们如果对其他表进行操作,大致内容也一致,只是java类和数据表不一致。这就能用到代码生成。
老版的代码生成器需先引入依赖,再编写大量代码用于代码生成,因为较复杂,现在已不再使用。我们推荐插件MyBatisX,下载并配置对应的java类和数据表信息即可。
DB静态工具
如果我们在一个UserService类中需要注入DeptService的依赖,同时在DeptService中又需注入UserService的依赖,这就会导致多个Service类之间相互调用,像衔尾蛇一样形成循环。为解决这一问题我们可以调用静态方法,这样免去了依赖注入这一步。
MB还提供了一系列与IService中名字相同的静态方法,与之不同是传入的参数多了一个java类的类型,使用这种方法就可以避免循环依赖注入,接下来演示部分代码(只示范ServiceImpl)。
java
//ServiceImpl------------------------------------------------------------------------------------------------------------
@Override
public UserList getUADById(Long id){
//查询用户
UserList targetUser= this.getById(id);
if(targetUser==null)
throw new RuntimeException("用户不存在");
//查询地址
List<Address> targetList = Db.lambdaQuery(Address.class)
// 使用Db工具类执行查询并获取地址列表
.eq(Address::getUserId, id).list();
//返回列表
targetUser.setAddress(BeanUtil.copyToList(address,Address.class));
}
因此一旦涉及到多个Service相互调用,我们可以使用DB静态工具来解决这一问题。
逻辑删除
有时需要删除某个数据但数据库仍需保存该数据,例如淘宝删除订单,用户端虽已无法查询到该订单数据,但在数据库中其仍然存在。
我们可以通过在数据表中添加一个特定的字段来标记数据是否被"删除"。通常,这个字段的类型为布尔值或者整数,用不同的值(如0和1)来表示数据的不同状态(如未删除和已删除)。
这样我们所有的delete方法都要变更为update方法,这会非常麻烦。
此时就用到了MB提供的逻辑删除功能,无需改变方法调用的方式,其会在底层帮我们自动修改CRUD语句,我们要做的就是在application.yml文件中配置逻辑删除的字段名和值即可。
# application.yml配置逻辑删除字段
mybatis-plus:
global-config:
db-config:
logic-delete-field: flag #管理逻辑删除的实例字段名
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
同时逻辑删除本身也有自己的问题:
- 会导致数据库表垃圾数据越来越多,影响查询效率。
- SQL中全都需要对逻辑删除字段做判断,影响查询效率。
因此不太推荐采用逻辑删除功能,建议如果数据需要删除,可以将数据迁移到其它表进行备份。
枚举处理器
上文我们提到了可以以某个属性值的不同来传递不同的信息,但使用的仍为1、2、3、4这样1的int型变量,第一眼并不能意识到这些数字代表什么,我们可以通过枚举来解决这一问题。
为了让Mybatis-Plus能够自动地将枚举类型与数据库中的数据类型进行转换,需要在配置文件application.yml中配置全局枚举处理器:
java
mybatis-plus:
configuration:
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
同时因为枚举类中有多个字段,需要添加到数据库中的属性就那么一两个,我们可以通过注解可以使用注解@EnumValue来表示这些字段需要加入到数据库中。 此时响应的数九就不再是1和2,而是NORMAL和CANCEL,如果感觉还不够直观,想要返回desc,即返回字段"正常"和"取消",我们只需在desc属性上加上注解@JsonValue即可。
java
//定义枚举类
@Getter
public enum OrderState {
NORMAL(0, "正常"),
CANCEL(1, "取消"),
DELETE(2, "删除");
@EnumValue // 用于数据库存储
private Integer code;
@JsonValue // 用于返回的json数据
private String desc;
OrderState(Integer code, String desc) {
this.code = code;
this.desc = desc;
}
}
//使用
User user = new User();
user.setState(OrderState.NORMAL);
JSON处理器
同理之前的json数据我们一般使用String接收,例如{"id":1,"name":"名字"},现在可直接使用json接收。需要两步:
首先给对应的java字段定义实体类,在该字段上加上注解来指定JSON类型处理器(处理器一个有三种,其使用的第三方框架不同,执行效果相同),然后给该该java类添加注解@TableName(value = "user", autoResultMap = true)来指定属性autoResultMap = true来开启自动映射。
java
@TableName(value = "user", autoResultMap = true)
public class User {
private Long id;
private String username;
@TableField(typeHandler = JacksonTypeHandler.class)
private UserInfo info;
}
插件功能
MB提供了很多内置拦截器(插件),有一个"分页插件"我们最常使用,之前我们都是通过pagehelper来完成分页功能,虽然其与MB不冲突,但因为其需要额外引入依赖,会导致代码不够简洁。我们可以直接使用MB提供的分页插件以简化代码。
首先,要在配置类中注册MyBatisPlus的核心插件,同时添加分页插件:
java
//配置类用于注册MyBatisPlus的核心插件和分页插件
@Configuration
public class MybatisConfig {
//注册MyBatisPlus拦截器和分页插件
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
// 初始化核心插件
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件
PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
paginationInterceptor.setMaxLimit(1000L); // 设置分页最大限制
interceptor.addInnerInterceptor(paginationInterceptor);
return interceptor;
}
}
配置好分页插件后,你可以在 Service 层使用 MyBatis-Plus 提供的Page对象来实现分页查询:
java
@Test
void testPageQuery() {
int pageNo = 1, pageSize = 2; //模拟前端传参
// 分页条件
Page<User> page = Page.of(pageNo, pageSize); // 创建分页对象
// 排序条件
page.addOrder(new OrderItem("balance", true)); // 根据余额升序排序
page.addOrder(new OrderItem("id", true)); // 根据ID升序排序
// 分页查询
Page<User> p = userService.page(page); // 调用UserService的page方法进行分页查询
}