刚才的案例中都是以id
为条件的简单CRUD
,一些复杂条件的SQL
语句就要用到一些更高级的功能了。
除了新增以外,修改、删除、查询的SQL
语句都需要指定where
条件。因此BaseMapper
中提供的相关方法除了以id
作为where
条件以外,还支持更加复杂的where
条件。
参数中的Wrapper
就是条件构造的抽象类,其下有很多默认实现,继承关系如图:
Wrapper
的子类AbstractWrapper
提供了where
中包含的所有条件构造方法:
而QueryWrapper
在AbstractWrapper
的基础上拓展了一个select
方法,允许指定查询字段:
而UpdateWrapper
在AbstractWrapper
的基础上拓展了一个set
方法,允许指定SQL
中的SET
部分:
无论是修改、删除、查询,都可以使用QueryWrapper
来构建查询条件。接下来看一些例子:
1.查询:查询出名字中带o
的,存款大于等于1000
元的人。代码如下:
@Test
void testQueryWrapper() {
// 1.构建查询条件 where name like "%o%" AND balance >= 1000
QueryWrapper<User> wrapper = new QueryWrapper<User>()
.select("id", "username", "info", "balance")
.like("username", "o")
.ge("balance", 1000);
// 2.查询数据
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}
更新:更新用户名为Jack
的用户的余额为2000
,代码如下:
@Test
void testUpdateByQueryWrapper() {
// 1.构建查询条件 where name = "Jack"
QueryWrapper<User> wrapper = new QueryWrapper<User>().eq("username", "Jack");
// 2.更新数据,user中非null字段都会作为set语句
User user = new User();
user.setBalance(2000);
userMapper.update(user, wrapper);
}
基于BaseMapper
中的update
方法更新时只能直接赋值,对于一些复杂的需求就难以实现。
例如:更新id
为1,2,4
的用户的余额,扣200
,对应的SQL
应该是:
UPDATE user SET balance = balance - 200 WHERE id in (1, 2, 4)
SET
的赋值结果是基于字段现有值的,这个时候就要利用UpdateWrapper
中的setSql
功能了:
// 更新id为1,2,4的用户的余额,扣200
@Test
void testUpdateWrapper() {
// List<Long> ids = new ArrayList<>(Arrays.asList(1L, 2L, 4L)); //JDK8 可以使用这种方式
List<Long> ids = List.of(1L, 2L, 4L); // JDK9 之后才有的of方法
// 1.生成SQL
UpdateWrapper<User> userUpdateWrapper = new UpdateWrapper<User>()
.setSql("balance = balance -200")
.in("id", ids); // WHERE id in (1, 2, 4)
// 2.更新,注意第一个参数可以给null,也就是不填更新字段和数据,
// 而是基于UpdateWrapper中的setSql来更新
userMapper.update(null, userUpdateWrapper);
}
无论是QueryWrapper
还是UpdateWrapper
在构造条件的时候都需要写死字段名称,会出现字符串魔法值
。这在编程规范中显然是不推荐的。
那怎么样才能不写字段名,又能知道字段名呢?
其中一种办法是基于变量的gettter
方法结合反射技术。因此我们只要将条件对应的字段的getter
方法传递给MybatisPlus
,它就能计算出对应的变量名了。而传递方法可以使用JDK8
中的方法引用和Lambda
表达式。
因此MybatisPlus
又提供了一套基于Lambda的Wrapper
,包含两个:
-LambdaQueryWrapper
LambdaUpdateWrapper
QueryWrapper
和UpdateWrapper
其使用方式如下:
@Test
void testLambdaQueryWrapper() {
// 1.构建查询条件 where name like "%o%" AND balance >= 1000
LambdaQueryWrapper<User> userLambdaQueryWrapper = 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(userLambdaQueryWrapper);
users.forEach(System.out::println);
}
// 更新用户名为jack的用户的余额为2000
@Test
void testLambdaUpdateByQueryWrapper() {
// 1.构建查询条件 where name = "Jack"
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>()
.eq(User::getUsername, "Jack");
// 2.更新数据,user中非null字段都会作为set语句
User user = new User();
user.setBalance(2000);
userMapper.update(user, wrapper);
}
在演示UpdateWrapper
的案例中,我们在代码中编写了更新的SQL
语句:
这种写法在某些企业也是不允许的,因为SQL
语句最好都维护在持久层,而不是业务层。就当前案例来说,由于条件是in
语句,只能将SQL
写在Mapper.xml
文件,利用foreach
来生成动态SQL
。
这实在是太麻烦了。假如查询条件更复杂,动态SQL
的编写也会更加复杂。
所以,MybatisPlus
提供了自定义SQL
功能,可以让我们利用Wrapper
生成查询条件,再结合Mapper.xml
编写SQL
以当前案例来说,我们可以这样写:
@Test
void testCustomWrapper() {
// 1.准备自定义查询条件
List<Long> ids = List.of(1L, 2L, 4L);
QueryWrapper<User> wrapper = new QueryWrapper<User>().in("id", ids);
// 2.调用mapper的自定义方法,直接传递Wrapper
userMapper.deductBalanceByIds(200, wrapper);
}
然后在UserMapper
中自定义SQL
:
package com.itheima.mp.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.mp.domain.po.User;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
import org.apache.ibatis.annotations.Param;
public interface UserMapper extends BaseMapper<User> {
@Select("UPDATE user SET balance = balance - #{money} ${ew.customSqlSegment}")
void deductBalanceByIds(@Param("money") int money, @Param("ew") QueryWrapper<User> wrapper);
}
这样就省去了编写复杂查询条件的烦恼了。
在
mapper
方法参数中用Param
注解声明wrapper
变量名称,必须是ew
。
为什么必须要用ew
?mybatisPlus
源码规定的,这也成为一种约定俗成的规范。当然,你也可以手动修改生成的代码,将ew
改为其他名字,但这样可能导致一些示例代码不适用。
理论上来讲MyBatisPlus
是不支持多表查询的,不过我们可以利用Wrapper
中自定义条件结合自定义SQL
来实现多表查询的效果。
例如,我们要查询出所有收货地址在北京的并且用户id
在1、2、4
之中的用户
要是自己基于mybatis
实现SQL
,大概是这样的:
<select id="queryUserByIdAndAddr" resultType="com.itheima.mp.domain.po.User">
SELECT *
FROM user u
INNER JOIN address a ON u.id = a.user_id
WHERE u.id
<foreach collection="ids" separator="," item="id" open="IN (" close=")">
#{id}
</foreach>
AND a.city = #{city}
</select>
可以看出其中最复杂的就是WHERE
条件的编写,如果业务复杂一些,这里的SQL
会更变态。
但是基于自定义SQL
结合Wrapper
的玩法,我们就可以利用Wrapper
来构建查询条件,然后手写SELECT
及FROM
部分,实现多表查询。
查询条件这样来构建:
@Test
void testCustomJoinWrapper() {
// 1.准备自定义查询条件
QueryWrapper<User> wrapper = new QueryWrapper<User>()
.in("u.id", List.of(1L, 2L, 4L))
.eq("a.city", "北京");
// 2.调用mapper的自定义方法
List<User> users = userMapper.queryUserByWrapper(wrapper);
users.forEach(System.out::println);
}
然后在UserMapper
中自定义方法:
@Select("SELECT u.* FROM user u INNER JOIN address a ON u.id = a.user_id ${ew.customSqlSegment}")
List<User> queryUserByWrapper(@Param("ew")QueryWrapper<User> wrapper);
当然,也可以在UserMapper.xml
中写SQL
:
<select id="queryUserByWrapper" resultType="com.itheima.mp.domain.po.User">
SELECT * FROM user u INNER JOIN address a ON u.id = a.user_id ${ew.customSqlSegment}
</select>
MybatisPlus
不仅提供了BaseMapper
,还提供了通用的Service
接口及默认实现,封装了一些常用的service
模板方法。
通用接口为IService
,默认实现为ServiceImpl
,其中封装的方法可以分为以下几类:
save
:新增remove
:删除update
:更新get
:查询单个结果list
:查询集合结果count
:计数page
:分页查询我们先俩看下基本的CRUD
接口。
新增:
save
是新增单个元素saveBatch
是批量新增saveOrUpdate
是根据id
判断,如果数据存在就更新,不存在则新增saveOrUpdateBatch
是批量的新增或修改删除:
removeById
:根据id
删除removeByIds
:根据id
批量删除removeByMap
:根据Map
中的键值对为条件删除remove(Wrapper<T>)
:根据Wrapper
条件删除removeBatchByIds
修改:
updateById
:根据id
修改update(Wrapper<T>)
:根据UpdateWrapper
修改,Wrapper
中包含set
和where
部分update(T,Wrapper<T>)
:按照T内的数据修改与Wrapper
匹配到的数据updateBatchById
:根据id
批量修改查询
Get
:
getById
:根据id
查询1
条数据getOne(Wrapper<T>)
:根据Wrapper
查询1
条数据getBaseMapper
:获取Service
内的BaseMapper
实现,某些时候需要直接调用Mapper
内的自定义SQL
时可以用这个方法获取到Mapper
List
:
listByIds
:根据id
批量查询list(Wrapper<T>)
:根据Wrapper
条件查询多条数据list()
:查询所有Count
:
count()
:统计所有数量count(Wrapper<T>)
:统计符合Wrapper
条件的数据数量getBaseMapper
:
当我们在service
中要调用Mapper
中自定义SQL
时,就必须获取service
对应的Mapper
,就可以通过这个方法:
项目结构如下:
来到 UserService
中创建 测试类
接下来,我们快速实现下面4个接口:
首先,我们在项目中引入几个依赖:
pom.xml
<!--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>
然后在application.xml
需要配置swagger
信息:
knife4j:
enable: true
openapi:
title: 用户管理接口文档
description: "用户管理接口文档"
email: zhanghuyi@itcast.cn
concat: 墨苒孤
url: https://www.itcast.cn
version: v1.0.0
group:
default:
group-name: default
api-rule: package
api-rule-resources:
- com.itheima.mp.controller
Alt + 8
选择 springboot
服务,启动,浏览器输入 http://localhost:8080/doc.html#/home
然后,接口需要两个实体:
UserFormDTO
:代表新增时的用户表单UserVO
:代表查询的返回结果UserFormDTO
:package com.itheima.mp.domain.dto;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel(description = "用户表单实体")
public class UserFormDTO {
@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;
}
然后是UserVO
:
package com.itheima.mp.domain.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel(description = "用户VO实体")
public class UserVO {
@ApiModelProperty("用户id")
private Long id;
@ApiModelProperty("用户名")
private String username;
@ApiModelProperty("详细信息")
private String info;
@ApiModelProperty("使用状态(1正常 2冻结)")
private Integer status;
@ApiModelProperty("账户余额")
private Integer balance;
}
用 @Autowired
注入,Spring
并不推荐,而是推荐我们使用构造函数注入
构造函数注入如下:
但是这样会带来一个问题,如果成员很多,构造函数就会很多,看起来很繁琐。那么就可以使用Lombok
注解帮我们简化
上图这样看起来是可以了,但是,一个类可以有很多的成员,并不是每一个都是需要注入的。那么怎么区分哪些需要注入,哪些不需要注入呢?
可以给 成员 加上 final
来区分, 这样就必须在类初始化的时候,对加了 final
的成员进行初始化,所以最终写法如下:
@RequiredArgsConstructor
表示 会对 final
修饰的成员进行创建构造函数,没有修饰就不会生成构造函数
新增接口实现:
@PostMapping
@ApiOperation("新增用户")
public void saveUser(@RequestBody UserFormDTO userFormDTO) {
// 1.转换DTO为PO
User user = BeanUtil.copyProperties(userFormDTO, User.class);
// 2.新增
userService.save(user);
}
由于传入的 是 DTO
, 而我们需要保存的是 PO
,所以需要先把DTO
转换为PO
。这里使用胡图工具包,其余接口同理
@DeleteMapping("/{id}")
@ApiOperation("删除用户")
public void removeUserById(@PathVariable("id") Long userId){
userService.removeById(userId);
}
@GetMapping("/{id}")
@ApiOperation("根据id查询用户")
public UserVO queryUserById(@PathVariable("id") Long userId){
// 1.查询用户
User user = userService.getById(userId);
// 2.处理po转vo
return BeanUtil.copyProperties(user, UserVO.class);
}
@GetMapping
@ApiOperation("根据id集合查询用户")
public List<UserVO> queryUserByIds(@RequestParam("ids") List<Long> ids){
// 1.查询用户
List<User> users = userService.listByIds(ids);
// 2.处理po转vo (集合用copyToList)
return BeanUtil.copyToList(users, UserVO.class);
}
可以看到上述接口都直接在controller
即可实现,无需编写任何service
代码,非常方便。
不过,一些带有业务逻辑的接口则需要在service
中自定义实现了。例如下面的需求:
id
扣减用户余额这看起来是个简单修改功能,只要修改用户余额即可。但这个业务包含一些业务逻辑处理:
这些业务逻辑都要在service
层来做,另外更新余额需要自定义SQL
,要在mapper
中来实现。因此,我们除了要编写controller
以外,具体的业务还要在service
和mapper
中编写。
首先在UserController
中定义一个方法:
@PutMapping("{id}/deduction/{money}")
@ApiOperation("扣减用户余额")
public void deductBalance(@PathVariable("id") Long id, @PathVariable("money")Integer money){
userService.deductBalance(id, money);
}
然后是UserService
接口:
package com.itheima.mp.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.mp.domain.po.User;
public interface IUserService extends IService<User> {
void deductBalance(Long id, Integer money);
}
最后是UserServiceImpl
实现类:
这里需要注意的是 在service
中 可以使用 this
来调用mybatisplus
提供给我们的 service
方法,因此下面的getById
就是mybatisplus
的提供的service
方法, 也可以使用baseMapper
来调用 mappeer
package com.itheima.mp.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.mapper.UserMapper;
import com.itheima.mp.service.IUserService;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Override
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.扣减余额(这个deductMoneyById方法是自定义的)
baseMapper.deductMoneyById(id, money);
}
}
测试接口
如果需要业务判断就得在 service
中编写,如果mybatisplus
提供的mapper不满足我们的需求就得写自定义mappper
IService
中还提供了Lambda
功能来简化我们的复杂查询及更新功能。我们通过两个案例来学习一下。
案例一:实现一个根据复杂条件查询用户的接口,查询条件如下:
name
:用户名关键字,可以为空status
:用户状态,可以为空minBalance
:最小余额,可以为空maxBalance
:最大余额,可以为空如果用传统写法,差不多长这样
我们首先需要定义一个查询条件实体,UserQuery
实体:
package com.itheima.mp.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;
}
接下来我们在UserController
中定义一个controller
方法:
@GetMapping("/list")
@ApiOperation("根据id集合查询用户")
public List<UserVO> queryUsers(UserQuery query){
// 1.组织条件
String username = query.getName();
Integer status = query.getStatus();
Integer minBalance = query.getMinBalance();
Integer maxBalance = query.getMaxBalance();
LambdaQueryWrapper<User> wrapper = new QueryWrapper<User>().lambda()
.like(username != null, User::getUsername, username)
.eq(status != null, User::getStatus, status)
.ge(minBalance != null, User::getBalance, minBalance)
.le(maxBalance != null, User::getBalance, maxBalance);
// 2.查询用户
List<User> users = userService.list(wrapper);
// 3.处理vo
return BeanUtil.copyToList(users, UserVO.class);
}
在组织查询条件的时候,我们加入了 username != null
这样的参数,意思就是当条件成立时才会添加这个查询条件,类似Mybatis
的mapper.xml
文件中的<if>
标签。这样就实现了动态查询条件效果了。
不过,上述条件构建的代码太麻烦了。
因此Service
中对LambdaQueryWrapper
和LambdaUpdateWrapper
的用法进一步做了简化。 我们无需自己通过new
的方式来创建Wrapper
,而是直接调用lambdaQuery
和lambdaUpdate
方法:
基于Lambda
查询:
@GetMapping("/list")
@ApiOperation("根据id集合查询用户")
public List<UserVO> queryUsers(UserQuery query){
// 1.组织条件
String username = query.getName();
Integer status = query.getStatus();
Integer minBalance = query.getMinBalance();
Integer maxBalance = query.getMaxBalance();
// 2.查询用户
List<User> users = userService.lambdaQuery()
.like(username != null, User::getUsername, username)
.eq(status != null, User::getStatus, status)
.ge(minBalance != null, User::getBalance, minBalance)
.le(maxBalance != null, User::getBalance, maxBalance)
.list();
// 3.处理vo
return BeanUtil.copyToList(users, UserVO.class);
}
可以发现lambdaQuery
方法中除了可以构建条件,还需要在链式编程的最后添加一个list()
,这是在告诉MP
我们的调用结果需要是一个list
集合。这里不仅可以用list()
,可选的方法有:
.one()
:最多1个结果.list()
:返回集合结果.count()
:返回计数结果MybatisPlus
会根据链式编程的最后一个方法来判断最终的返回结果。与lambdaQuery
方法类似,IService
中的lambdaUpdate
方法可以非常方便的实现复杂更新业务。
例如下面的需求:
需求:改造根据id修改用户余额的接口,要求如下
- 如果扣减后余额为0,则将用户status修改为冻结状态(2)
也就是说我们在扣减用户余额时,需要对用户剩余余额做出判断,如果发现剩余余额为0
,则应该将status
修改为2
,这就是说update
语句的set
部分是动态的。
实现如下:
@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.扣减余额 update tb_user set balance = balance - ?
// 4.1.先计算剩余的余额
int remainBalance = user.getBalance() - money;
lambdaUpdate()
.set(User::getBalance, remainBalance) // 更新余额
.set(remainBalance == 0, User::getStatus, 2) // 动态判断,是否更新status
.eq(User::getId, id)
.eq(User::getBalance, user.getBalance()) // 乐观锁
.update();
}
由于我们上面是先查用户,再对查到的用户进行更新数据,如果有多个线程进来,会有并发风险,假设有两个线程同时进来,同时都是扣减100块钱
,由于操作的是同一个,最终只会扣减100块
因此需要加上锁,这里加的是乐观锁,是先比较再更新 .eq(User::getBalance, user.getBalance())
,判断用户的余额是否为查到的余额,如果是,才进行修改,最后在方法上加上事务注解@Transactional
如果条件比较复杂,可以使用 lambda
来构造sql
语句
IService
中的批量新增功能使用起来非常方便,但有一点注意事项,我们先来测试一下。
首先我们测试逐条插入数据:
@Test
void testSaveOneByOne() {
long b = System.currentTimeMillis();
for (int i = 1; i <= 100000; i++) {
userService.save(buildUser(i));
}
long e = System.currentTimeMillis();
System.out.println("耗时:" + (e - b));
}
private User buildUser(int i) {
User user = new User();
user.setUsername("user_" + i);
user.setPassword("123");
user.setPhone("" + (18688190000L + i));
user.setBalance(2000);
user.setInfo("{\"age\": 24, \"intro\": \"英文老师\", \"gender\": \"female\"}");
user.setCreateTime(LocalDateTime.now());
user.setUpdateTime(user.getCreateTime());
return user;
}
执行结果如下:
可以看到速度非常慢。
然后再试试MybatisPlus
的批处理:
@Test
void testSaveBatch() {
// 准备10万条数据
List<User> list = new ArrayList<>(1000);
long b = System.currentTimeMillis();
for (int i = 1; i <= 100000; i++) {
list.add(buildUser(i));
// 每1000条批量插入一次
if (i % 1000 == 0) {
userService.saveBatch(list);
list.clear();
}
}
long e = System.currentTimeMillis();
System.out.println("耗时:" + (e - b));
}
执行最终耗时如下:
可以看到使用了批处理以后,比逐条新增效率提高了10倍左右,性能还是不错的。
不过,我们简单查看一下MybatisPlus
源码:
@Transactional(rollbackFor = Exception.class)
@Override
public boolean saveBatch(Collection<T> entityList, int batchSize) {
String sqlStatement = getSqlStatement(SqlMethod.INSERT_ONE);
return executeBatch(entityList, batchSize, (sqlSession, entity) -> sqlSession.insert(sqlStatement, entity));
}
// ...SqlHelper
public static <E> boolean executeBatch(Class<?> entityClass, Log log, Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) {
Assert.isFalse(batchSize < 1, "batchSize must not be less than one");
return !CollectionUtils.isEmpty(list) && executeBatch(entityClass, log, sqlSession -> {
int size = list.size();
int idxLimit = Math.min(batchSize, size);
int i = 1;
for (E element : list) {
consumer.accept(sqlSession, element);
if (i == idxLimit) {
sqlSession.flushStatements();
idxLimit = Math.min(idxLimit + batchSize, size);
}
i++;
}
});
}
可以发现其实MybatisPlus
的批处理是基于PrepareStatement
的预编译模式,然后批量提交,最终在数据库执行时还是会有多条insert
语句,逐条插入数据。SQL
类似这样:
Preparing: INSERT INTO user ( username, password, phone, info, balance, create_time, update_time ) VALUES ( ?, ?, ?, ?, ?, ?, ? )
Parameters: user_1, 123, 18688190001, "", 2000, 2023-07-01, 2023-07-01
Parameters: user_2, 123, 18688190002, "", 2000, 2023-07-01, 2023-07-01
Parameters: user_3, 123, 18688190003, "", 2000, 2023-07-01, 2023-07-01
就好比上面我分10
次插入,就会有10
条insert
的sql
语句
而如果想要得到最佳性能,最好是将多条SQL
合并为一条,像这样:
INSERT INTO user ( username, password, phone, info, balance, create_time, update_time )
VALUES
(user_1, 123, 18688190001, "", 2000, 2023-07-01, 2023-07-01),
(user_2, 123, 18688190002, "", 2000, 2023-07-01, 2023-07-01),
(user_3, 123, 18688190003, "", 2000, 2023-07-01, 2023-07-01),
(user_4, 123, 18688190004, "", 2000, 2023-07-01, 2023-07-01);
该怎么做呢?
MySQL
的客户端连接参数中有这样的一个参数:rewriteBatchedStatements
。顾名思义,就是重写批处理的statement
语句。参考文档:
https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-connp-props-performance-extensions.html#cj-conn-prop_rewriteBatchedStatements
这个参数的默认值是false
,我们需要修改连接参数,将其配置为true
修改项目中的application.yml
文件,在jdbc
的url
后面添加参数&rewriteBatchedStatements=true
:
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
再次测试插入10
万条数据,可以发现速度有非常明显的提升:
在ClientPreparedStatement
的executeBatchInternal
中,有判断rewriteBatchedStatements
值是否为true
并重写SQL
的功能:
最终,10
万条数据的插入,SQL
被重写为一条sql
语句。
代码在github
https://github.com/RanGuMo/mp-demo