Springboot+MyBatis批量插入数据的三种方式

1. 背景介绍

在开发过程中,我们经常会遇到往数据库表中插入大量数据的场景,比如excel批量导入数据。那么该如何快速地插入数据呢?

我们可以考虑使用批量插入来实现,实测100000条数据添加,后附具体实现代码。

2. 方案介绍

2.1 第一种方案,用 for语句循环插入(不推荐)

用一个 for 循环,把数据一条一条地插入。

insert into t_user values (?, ?, ?, ?, ?)
/**
* 第一种方案,用 for语句循环插入 10万 条数据
*/
@GetMapping("/test1")
public String test1(int count) {
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    for (int i = 0; i < count; i++) {
        User user = new User();
        user.setName("方案1测试" + i);
        user.setGender("男");
        user.setUsername("方案1测试");
        user.setPassword("方案1测试");
        user.setRemark("方案1测试");
        userService.saveInfo(user);
    }
    stopWatch.stop();
    System.out.println("第一种方案,用 for语句循环插入耗时:" + stopWatch.getTotalTimeMillis());
    return "操作完成";
}

优势:JDBC 中的 PreparedStatement 有预编译功能,预编译之后会缓存起来。

之后SQL执行会比较快,且 JDBC可以开启批处理,这个批处理执行非常给力。

劣势:这种方式插入大量数据时,效率非常底下,不推荐。很多时候我们的 SQL 服务器和应用服务器可能并不是同一台,所以必须要考虑网络 IO。

如果网络 IO 比较费时间的话,那么可能会拖慢 SQL 执行的速度。

2.2 第二种方案,利用mybatis的foreach来实现循环插入(不推荐)

insert into t_user values (?, ?, ?, ?, ?) , (?, ?, ?, ?, ?) , (?, ?, ?, ?, ?)
/**
* 第二种方案,利用mybatis的foreach来实现循环插入 10万 条数据
*/
@GetMapping("/test2")
public String test2(int count) {
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    List<User> list = new ArrayList<>();
    for (int i = 0; i < count; i++) {
        User user = new User();
        user.setName("方案2测试" + i);
        user.setGender("男");
        user.setUsername("方案2测试");
        user.setPassword("方案2测试");
        user.setRemark("方案2测试");
        list.add(user);
    }
    userService.saveList(list);
    stopWatch.stop();
    System.out.println("第二种方案,利用mybatis的foreach来实现循环插入耗时:" + stopWatch.getTotalTimeMillis());
    return "操作完成";
}
<insert id="saveList" parameterType="list">
    insert into t_user values
    <foreach collection="list" item="item" separator=",">
        (#{item.name}, #{item.gender}, #{item.username}, #{item.password}, #{item.remark})
    </foreach>
</insert>

优势:不用频繁访问数据库,一条sql搞定,效率比较高。

劣势:一当数据量太大时,会出现拼接的sql语句超长而执行失败,所以当数据量太大时,也不推荐。

二是 SQL 太长了,甚至可能需要分片后批量处理。

三是无法充分发挥 PreparedStatement 预编译的优势,SQL 要重新解析且无法复用

com.mysql.cj.jdbc.exceptions.PacketTooBigException: Packet for query is too large (4,879,714 > 4,194,304).
    You can change this value on the server by setting the 'max_allowed_packet' variable.

2.3 第三种方案,使用sqlSessionFactory实现批量插入(推荐)

@Resource
private SqlSessionFactory sqlSessionFactory;
// 关闭session的自动提交
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH, false);
try {
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    list.stream().forEach(user -> userMapper.saveInfo(user));
    // 提交数据
    sqlSession.commit();
    sqlSession.rollback();
} catch (Exception e) {
    sqlSession.rollback();
} finally {
    sqlSession.close();
}

优势:这种方式可以说是集第一种和第二种方式的优点于一身,既可以提高运行效率,又可以保证大数据量时执行成功,大数据量时推荐使用这种方式。

4. 创建Springboot项目

4.1 选择Spring Initializr(springboot项目)

img

4.2 配置属性,完成点击next

img

4.3 项目启动类

img

4.4 Pom文件添加依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.5</version>
        <relativePath/>
    </parent>

    <groupId>com.liyh</groupId>
    <artifactId>springboot_mybatis</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot_mybatis</name>
    <description>springboot_mybatis</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--mysql驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!--jdbc 数据库连接-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <!--引入阿里数据库连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.6</version>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!--mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

4.5 配置application.yml文件

# 配置端口
server:
  port: 8091

spring:
  # 配置数据源
  datasource:
    url: jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=UTC
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver

# mybatis相关配置
mybatis:
  mapper-locations: classpath*:mapper/*.xml   #指定mapper映射文件路径
  type-aliases-package: com.liyh.mybatis.entity  # 别名
  configuration:
    map-underscore-to-camel-case: true

#打印sql,保存到文件
logging:
  level:
    com.liyh.mybatis.mapper: debug

4.6 导入数据库

CREATE TABLE `t_user`  (
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `gender` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `remark` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '批量导入用户表' ROW_FORMAT = COMPACT;

4.7 项目结构

img

5. 方案测试

5.1 测试第一种方案,用 for语句循环插入 10万 条数据

1. 通过postman调用接口

img

2. 查看耗时情况(20万条数据需要10几分钟!!!)

img

img

3. 数据库数据

img

5.2 测试第二种方案,用 for语句循环插入 10万 条数据

1. 通过postman调用接口

img

2. 查看耗时情况(测试2000,10000,50000,100000条数据)

img

img

img

img

当数据量达到5万条时,报错了!!!(单批次少量数据效率还可以)

img

### Cause: com.mysql.cj.jdbc.exceptions.PacketTooBigException: Packet for query is too large (5,238,915 > 4,194,304). You can change this value on the server by setting the 'max_allowed_packet' variable.
; Packet for query is too large (5,238,915 > 4,194,304). You can change this value on the server by setting the 'max_allowed_packet' variable.; nested exception is com.mysql.cj.jdbc.exceptions.PacketTooBigException: Packet for query is too large (5,238,915 > 4,194,304). You can change this value on the server by setting the 'max_allowed_packet' variable.] with root cause

com.mysql.cj.jdbc.exceptions.PacketTooBigException: Packet for query is too large (5,238,915 > 4,194,304). You can change this value on the server by setting the 'max_allowed_packet' variable.
	at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:107) ~[mysql-connector-j-8.0.31.jar:8.0.31]
	at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:916) ~[mysql-connector-j-8.0.31.jar:8.0.31]
	at com.mysql.cj.jdbc.ClientPreparedStatement.execute(ClientPreparedStatement.java:354) ~[mysql-connector-j-8.0.31.jar:8.0.31]
	at com.zaxxer.hikari.pool.ProxyPreparedStatement.execute(ProxyPreparedStatement.java:44) ~[HikariCP-4.0.3.jar:na]
	at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.execute(HikariProxyPreparedStatement.java) ~[HikariCP-4.0.3.jar:na]
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_201]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_201]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_201]
	at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_201]
	at org.apache.ibatis.logging.jdbc.PreparedStatementLogger.invoke(PreparedStatementLogger.java:59) ~[mybatis-3.5.9.jar:3.5.9]
	at com.sun.proxy.$Proxy72.execute(Unknown Source) ~[na:na]
	at org.apache.ibatis.executor.statement.PreparedStatementHandler.update(PreparedStatementHandler.java:47) ~[mybatis-3.5.9.jar:3.5.9]
	at org.apache.ibatis.executor.statement.RoutingStatementHandler.update(RoutingStatementHandler.java:74) ~[mybatis-3.5.9.jar:3.5.9]
	at org.apache.ibatis.executor.SimpleExecutor.doUpdate(SimpleExecutor.java:50) ~[mybatis-3.5.9.jar:3.5.9]
	at org.apache.ibatis.executor.BaseExecutor.update(BaseExecutor.java:117) ~[mybatis-3.5.9.jar:3.5.9]
	at org.apache.ibatis.executor.CachingExecutor.update(CachingExecutor.java:76) ~[mybatis-3.5.9.jar:3.5.9]

5.3 测试第三种方案,使用sqlSessionFactory实现批量插入 20万 条数据

1. 通过postman调用接口

img

2. 查看耗时情况(20万条数据大概17秒)

img

img

3. 数据库数据

img

6. 部分代码

6.1 UserController

package com.liyh.mybatis.controller;

import com.liyh.mybatis.entity.User;
import com.liyh.mybatis.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StopWatch;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;

/**
 * 测试接口
 *
 * @Author: liyh
 */
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    UserService userService;

    /**
     * 第一种方案,用 for语句循环插入 10万 条数据
     */
    @GetMapping("/test1")
    public String test1(int count) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        for (int i = 0; i < count; i++) {
            User user = new User();
            user.setName("方案1测试" + i);
            user.setGender("男");
            user.setUsername("方案1测试");
            user.setPassword("方案1测试");
            user.setRemark("方案1测试");
            userService.saveInfo(user);
        }
        stopWatch.stop();
        System.out.println("第一种方案,用 for语句循环插入耗时:" + stopWatch.getTotalTimeMillis());
        return "操作完成";
    }

    /**
     * 第二种方案,利用mybatis的foreach来实现循环插入 10万 条数据
     */
    @GetMapping("/test2")
    public String test2(int count) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        List<User> list = new ArrayList<>();
        for (int i = 0; i < count; i++) {
            User user = new User();
            user.setName("方案2测试" + i);
            user.setGender("男");
            user.setUsername("方案2测试");
            user.setPassword("方案2测试");
            user.setRemark("方案2测试");
            list.add(user);
        }
        userService.saveList(list);
        stopWatch.stop();
        System.out.println("第二种方案,利用mybatis的foreach来实现循环插入耗时:" + stopWatch.getTotalTimeMillis());
        return "操作完成";
    }

    /**
     * 第三种方案,使用sqlSessionFactory实现批量插入 10万 条数据
     */
    @GetMapping("/test3")
    public String test3(int count) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        List<User> list = new ArrayList<>();
        for (int i = 0; i < count; i++) {
            User user = new User();
            user.setName("方案3测试" + i);
            user.setGender("男");
            user.setUsername("方案3测试");
            user.setPassword("方案3测试");
            user.setRemark("方案3测试");
            list.add(user);
        }
        userService.saveBeach(list);
        stopWatch.stop();
        System.out.println("第三种方案,使用sqlSessionFactory实现批量插入:" + stopWatch.getTotalTimeMillis());
        return "操作完成";
    }

}

6.2 UserServiceImpl

package com.liyh.mybatis.service.impl;

import com.liyh.mybatis.entity.User;
import com.liyh.mybatis.mapper.UserMapper;
import com.liyh.mybatis.service.UserService;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.util.List;

/**
 * 用户业务实现类
 *
 * @Author: liyh
 */
@Service
@Transactional
public class UserServiceImpl implements UserService {

    @Resource
    private UserMapper userMapper;

    @Resource
    private SqlSessionFactory sqlSessionFactory;

    @Override
    public void saveInfo(User user) {
        userMapper.saveInfo(user);
    }

    @Override
    public void saveList(List<User> list) {
        userMapper.saveList(list);
    }

    @Override
    public void saveBeach(List<User> list) {
        // ExecutorType.SIMPLE: 这个执行器类型不做特殊的事情。它为每个语句的执行创建一个新的预处理语句。
        // ExecutorType.REUSE: 这个执行器类型会复用预处理语句。
        // ExecutorType.BATCH: 这个执行器会批量执行所有更新语句,如果 SELECT 在它们中间执行还会标定它们是 必须的,来保证一个简单并易于理解的行为。

        // 关闭session的自动提交
        SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH, false);
        try {
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
            list.stream().forEach(user -> userMapper.saveInfo(user));
            // 提交数据
            sqlSession.commit();
            sqlSession.rollback();
        } catch (Exception e) {
            sqlSession.rollback();
        } finally {
            sqlSession.close();
        }
    }
}

6.3 UserMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.liyh.mybatis.mapper.UserMapper">

    <insert id="saveInfo">
        insert into t_user
        values (#{name}, #{gender}, #{username}, #{password}, #{remark})
    </insert>

    <insert id="saveList" parameterType="list">
        insert into t_user values
        <foreach collection="list" item="item" separator=",">
            (#{item.name}, #{item.gender}, #{item.username}, #{item.password}, #{item.remark})
        </foreach>
    </insert>


</mapper>
posted @ 2024-11-04 17:45  哩个啷个波  阅读(205)  评论(0编辑  收藏  举报