Spring Data JPA批量查询与批量写入的优化实战指南

 更新时间:2026年06月26日 08:29:41   作者:霸道流氓气质  
本文介绍了Spring Data JPA中批量查询与写入的优化方法,主要内容包括批量操作的必要性,批量查询API,批量写入优化等,感兴趣的小伙伴可以了解下

一、为什么需要批量优化

1.1 逐条操作的隐性成本

一次看似简单的 repository.findByCode(code) 调用,底层经历了以下过程:

应用代码 → 获取数据库连接(连接池)
         → 构建SQL → 网络传输到DB
         → DB解析+执行+返回结果
         → 网络传输回应用
         → 释放连接到连接池
         → ORM结果映射

单次耗时约 3-10ms,但循环 N 次后:

N(数据量)总耗时(@5ms/次)连接获取/释放次数
1000.5s100次
10005s1000次
500025s5000次
50000250s(超时)50000次

1.2 批量操作的本质

将 N 次 “请求-响应” 循环压缩为 1 次(或常数次),利用数据库 IN 查询一次性返回所有结果。

-- 逐条:执行N次
SELECT * FROM member_base WHERE seller_code = '8800001';
SELECT * FROM member_base WHERE seller_code = '8800002';
...
SELECT * FROM member_base WHERE seller_code = '8800N';
-- 批量:执行1次
SELECT * FROM member_base WHERE seller_code IN ('8800001','8800002',...,'8800N');

二、Spring Data JPA 批量查询 API

2.1 方法命名派生查询

public interface UserRepository extends JpaRepository<User, Integer> {

    // 单条查询(返回唯一结果,多条会抛NonUniqueResultException)
    User findByEmail(String email);

    // 单条查询(安全版,多条时取第一条)
    User findFirstByEmail(String email);

    // 批量IN查询(核心API)
    List<User> findByIdIn(List<Integer> ids);
    List<User> findByEmailIn(Collection<String> emails);
    List<User> findByStatusIn(Set<Integer> statuses);
}

2.2 Spring Data JPA 命名规则

关键字方法示例生成SQL
InfindByIdIn(List)WHERE id IN (?,?,?)
NotInfindByIdNotIn(List)WHERE id NOT IN (?,?,?)
BetweenfindByAgeBetween(a,b)WHERE age BETWEEN ? AND ?
FirstfindFirstByCode(code)LIMIT 1(避免多条异常)

2.3@Query自定义批量查询

public interface OrderRepository extends JpaRepository<Order, Long> {

    // JPQL方式
    @Query("SELECT o FROM Order o WHERE o.orderNo IN :orderNos")
    List<Order> findByOrderNos(@Param("orderNos") List<String> orderNos);

    // 原生SQL方式
    @Query(value = "SELECT * FROM orders WHERE status = :status AND create_time > :time",
           nativeQuery = true)
    List<Order> findRecentByStatus(@Param("status") Integer status, 
                                   @Param("time") Date time);
}

2.4findAllById内置方法

JPA 内置了 findAllById,适合按主键批量查询:

// JpaRepository 内置方法
List<User> users = userRepository.findAllById(Arrays.asList(1, 2, 3, 4, 5));

三、批量查询的底层原理

3.1IN查询的执行计划

EXPLAIN SELECT * FROM member_base WHERE seller_code IN ('88001','88002',...,'88500');
场景索引命中执行方式
seller_code 有索引,IN 数量 < 1000Index Range Scan
seller_code 有索引,IN 数量 > 1000可能退化可能全表扫描
seller_code 无索引Full Table Scan

3.2 MySQL IN 子句限制

限制维度默认值说明
max_allowed_packet64MB单次 SQL 最大字节数
IN 元素数量无硬性上限但超过 1000 个优化器可能不走索引
预编译参数数65535JDBC PreparedStatement 参数上限

最佳实践:每批 500-1000 个,超出分批查询。

3.3 Hibernate 的 IN 查询优化

Hibernate 5.2+ 引入了 IN clause padding,会将 IN 参数数量对齐到 2 的幂次方,增加 PreparedStatement 缓存命中率:

spring:
  jpa:
    properties:
      hibernate:
        query:
          in_clause_parameter_padding: true
-- 原始:IN (?,?,?,?,?) 5个参数
-- Padding后:IN (?,?,?,?,?,?,?,?) 8个参数(补null)
-- 下次 IN 6/7/8个参数时可复用同一PreparedStatement

四、批量写入 API 与优化

4.1saveAllvs 循环save

// ❌ 逐条保存:N次事务 + N次flush
for (User user : userList) {
    userRepository.save(user);
}

// ✅ 批量保存:1次事务 + 1次flush
userRepository.saveAll(userList);

4.2 Hibernate 批量 INSERT 配置

默认情况下 saveAll 仍可能逐条执行 INSERT,需配置批量参数:

spring:
  jpa:
    properties:
      hibernate:
        jdbc:
          batch_size: 500          # 每500条刷一次
        order_inserts: true        # 同类型INSERT排序后批量执行
        order_updates: true        # 同类型UPDATE排序后批量执行
        generate_statistics: true  # 开启统计(调试用)

配置后,Hibernate 会将多条 INSERT 合并为一次 JDBC executeBatch()

-- 未优化:逐条执行
INSERT INTO user (name,email) VALUES ('A','a@x.com');
INSERT INTO user (name,email) VALUES ('B','b@x.com');
-- 优化后:JDBC Batch
addBatch: INSERT INTO user (name,email) VALUES ('A','a@x.com');
addBatch: INSERT INTO user (name,email) VALUES ('B','b@x.com');
executeBatch(); // 一次网络往返

4.3 主键生成策略的影响

策略支持批量INSERT原因
IDENTITY(自增)Hibernate 需逐条 INSERT 获取生成的 ID
SEQUENCE(序列)可预分配 ID 段
TABLE(表生成)可预分配 ID 段
UUID(应用生成)无需数据库参与

注意:如果实体使用 @GeneratedValue(strategy = IDENTITY),即使配了 batch_size,Hibernate 也不会真正批量 INSERT。

五、批量查询结果转 Map 的模式

5.1 基础模式

// 查询结果 → Map<key, entity>
Map<String, User> userMap = userRepository.findByEmailIn(emails)
    .stream()
    .collect(Collectors.toMap(User::getEmail, u -> u));

// 使用:O(1) 查找
User user = userMap.get("test@example.com");

5.2 处理 key 重复

// 一个 key 对应多条记录时,取第一条
Map<String, User> map = list.stream()
    .collect(Collectors.toMap(User::getEmail, u -> u, (u1, u2) -> u1));

// 一个 key 对应多条记录时,按列表分组
Map<String, List<User>> groupMap = list.stream()
    .collect(Collectors.groupingBy(User::getDeptCode));

5.3 判断存在性用 Set

// 只需判断"是否存在"时,用 Set 更高效
Set<String> existingEmails = userRepository.findByEmailIn(emails)
    .stream()
    .map(User::getEmail)
    .collect(Collectors.toSet());

// O(1) 判断
if (existingEmails.contains("test@example.com")) { ... }

六、分批查询工具方法

6.1 通用分批查询工具

/**
 * 分批执行IN查询,避免单次IN过大.
 *
 * @param allKeys 所有待查询的key
 * @param batchSize 每批大小
 * @param queryFn 查询函数
 * @return 合并后的结果
 */
public static <K, R> List<R> batchQuery(List<K> allKeys, int batchSize,
        Function<List<K>, List<R>> queryFn) {
    if (allKeys == null || allKeys.isEmpty()) {
        return new ArrayList<>();
    }
    List<R> result = new ArrayList<>();
    for (int i = 0; i < allKeys.size(); i += batchSize) {
        List<K> batch = allKeys.subList(i, Math.min(i + batchSize, allKeys.size()));
        List<R> batchResult = queryFn.apply(batch);
        if (batchResult != null) {
            result.addAll(batchResult);
        }
    }
    return result;
}

使用方式:

List<User> allUsers = BatchUtil.batchQuery(
    allEmails, 500, 
    batch -> userRepository.findByEmailIn(batch)
);

6.2 分批查询 + 转 Map 组合

public static <K, V, R> Map<K, V> batchQueryToMap(
        List<K> allKeys, int batchSize,
        Function<List<K>, List<R>> queryFn,
        Function<R, K> keyMapper,
        Function<R, V> valueMapper) {
    List<R> allResults = batchQuery(allKeys, batchSize, queryFn);
    return allResults.stream()
        .collect(Collectors.toMap(keyMapper, valueMapper, (v1, v2) -> v1));
}

七、完整示例代码

以下以"学生成绩批量录入"为场景演示完整的批量优化实现。

7.1 实体定义

@Data
@Entity
@Table(name = "student")
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "student_no", unique = true)
    private String studentNo;

    @Column(name = "name")
    private String name;

    @Column(name = "class_code")
    private String classCode;

    @Column(name = "status")
    private Integer status;
}

@Data
@Entity
@Table(name = "score_record")
public class ScoreRecord {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "student_no")
    private String studentNo;

    @Column(name = "subject")
    private String subject;

    @Column(name = "score")
    private BigDecimal score;

    @Column(name = "exam_date")
    private Date examDate;

    @Column(name = "create_time")
    private Date createTime;
}

7.2 Repository 定义

public interface StudentRepository extends JpaRepository<Student, Long> {

    // 单条查询(可能抛 NonUniqueResultException)
    Student findByStudentNo(String studentNo);

    // 安全的单条查询
    Student findFirstByStudentNo(String studentNo);

    // ★ 批量IN查询
    List<Student> findByStudentNoIn(List<String> studentNos);

    // 按班级批量查询
    List<Student> findByClassCodeIn(List<String> classCodes);
}

public interface ScoreRecordRepository extends JpaRepository<ScoreRecord, Long> {

    // 批量查询已存在的成绩记录
    List<ScoreRecord> findByStudentNoInAndSubject(
            Collection<String> studentNos, String subject);
}

7.3 Service 实现(优化前 vs 优化后)

@Slf4j
@Service
public class ScoreImportServiceImpl implements ScoreImportService {

    @Resource
    private StudentRepository studentRepository;

    @Resource
    private ScoreRecordRepository scoreRecordRepository;

    /**
     * ❌ 优化前:逐条查询 + 逐条保存.
     * 5000条数据约需 35-60秒
     */
    public ImportResult importScoresSlow(List<ScoreExcelDto> dataList, String subject) {
        List<String[]> failList = new ArrayList<>();
        int successCount = 0;

        for (ScoreExcelDto dto : dataList) {
            // 每条数据 1 次DB查询
            Student student = studentRepository.findFirstByStudentNo(dto.getStudentNo());
            if (student == null) {
                failList.add(new String[]{dto.getStudentNo(), "学生不存在"});
                continue;
            }

            // 每条数据 1 次DB查询
            List<ScoreRecord> existing = scoreRecordRepository
                    .findByStudentNoInAndSubject(
                            Collections.singletonList(dto.getStudentNo()), subject);
            if (!existing.isEmpty()) {
                failList.add(new String[]{dto.getStudentNo(), "成绩已录入"});
                continue;
            }

            // 每条数据 1 次DB写入
            ScoreRecord record = new ScoreRecord();
            record.setStudentNo(dto.getStudentNo());
            record.setSubject(subject);
            record.setScore(dto.getScore());
            record.setExamDate(new Date());
            record.setCreateTime(new Date());
            scoreRecordRepository.save(record);
            successCount++;
        }

        return new ImportResult(successCount, failList.size());
    }

    /**
     * ✅ 优化后:批量预查询 + 内存校验 + 批量保存.
     * 5000条数据约需 3-8秒
     */
    @Override
    public ImportResult importScoresFast(List<ScoreExcelDto> dataList, String subject) {
        List<String[]> failList = new ArrayList<>();
        List<ScoreRecord> successList = new ArrayList<>();

        // ========== 第一步:提取并去重所有学号 ==========
        List<String> allStudentNos = dataList.stream()
                .map(ScoreExcelDto::getStudentNo)
                .filter(Objects::nonNull)
                .map(String::trim)
                .distinct()
                .collect(Collectors.toList());

        // ========== 第二步:批量预查询(分批,每批500) ==========

        // 2.1 批量查询学生信息 → Map
        Map<String, Student> studentMap = new HashMap<>();
        for (int i = 0; i < allStudentNos.size(); i += 500) {
            List<String> batch = allStudentNos.subList(i,
                    Math.min(i + 500, allStudentNos.size()));
            List<Student> students = studentRepository.findByStudentNoIn(batch);
            if (students != null) {
                for (Student s : students) {
                    studentMap.putIfAbsent(s.getStudentNo(), s);
                }
            }
        }

        // 2.2 批量查询已存在的成绩记录 → Set
        Set<String> existingStudentNos = new HashSet<>();
        for (int i = 0; i < allStudentNos.size(); i += 500) {
            List<String> batch = allStudentNos.subList(i,
                    Math.min(i + 500, allStudentNos.size()));
            List<ScoreRecord> existingRecords =
                    scoreRecordRepository.findByStudentNoInAndSubject(batch, subject);
            if (existingRecords != null) {
                for (ScoreRecord r : existingRecords) {
                    existingStudentNos.add(r.getStudentNo());
                }
            }
        }

        // ========== 第三步:内存中逐条校验(0次DB) ==========
        Set<String> batchDuplicate = new HashSet<>();

        for (ScoreExcelDto dto : dataList) {
            String studentNo = dto.getStudentNo();

            if (studentNo == null || studentNo.trim().isEmpty()) {
                failList.add(new String[]{studentNo, "学号不能为空"});
                continue;
            }
            String trimmedNo = studentNo.trim();

            // 批次内去重
            if (batchDuplicate.contains(trimmedNo)) {
                failList.add(new String[]{studentNo, "文件中学号重复"});
                continue;
            }

            // 学生是否存在(Map O(1))
            Student student = studentMap.get(trimmedNo);
            if (student == null) {
                failList.add(new String[]{studentNo, "学生不存在"});
                continue;
            }

            // 学生状态校验(内存判断)
            if (!Objects.equals(student.getStatus(), 1)) {
                failList.add(new String[]{studentNo, "学生已休学或退学"});
                continue;
            }

            // 成绩是否已录入(Set O(1))
            if (existingStudentNos.contains(trimmedNo)) {
                failList.add(new String[]{studentNo, "该科目成绩已录入"});
                continue;
            }

            // 校验通过,构建实体
            ScoreRecord record = new ScoreRecord();
            record.setStudentNo(trimmedNo);
            record.setSubject(subject);
            record.setScore(dto.getScore());
            record.setExamDate(new Date());
            record.setCreateTime(new Date());
            successList.add(record);
            batchDuplicate.add(trimmedNo);
        }

        // ========== 第四步:批量保存(1次DB) ==========
        int successCount = 0;
        if (!successList.isEmpty()) {
            try {
                scoreRecordRepository.saveAll(successList);
                successCount = successList.size();
            } catch (Exception e) {
                log.error("批量保存失败,降级为逐条", e);
                for (ScoreRecord record : successList) {
                    try {
                        scoreRecordRepository.save(record);
                        successCount++;
                    } catch (Exception ex) {
                        failList.add(new String[]{record.getStudentNo(),
                                "保存失败:" + ex.getMessage()});
                    }
                }
            }
        }

        return new ImportResult(successCount, failList.size(), failList);
    }
}

7.4 性能对比测试

@SpringBootTest
public class ScoreImportPerfTest {

    @Resource
    private ScoreImportService scoreImportService;

    @Test
    public void testPerformanceComparison() {
        // 准备5000条测试数据
        List<ScoreExcelDto> dataList = generateTestData(5000);

        // 逐条方式
        long start1 = System.currentTimeMillis();
        ImportResult result1 = scoreImportService.importScoresSlow(dataList, "数学");
        long cost1 = System.currentTimeMillis() - start1;

        // 批量方式
        long start2 = System.currentTimeMillis();
        ImportResult result2 = scoreImportService.importScoresFast(dataList, "英语");
        long cost2 = System.currentTimeMillis() - start2;

        System.out.println("逐条方式耗时: " + cost1 + "ms"); // ~35000-60000ms
        System.out.println("批量方式耗时: " + cost2 + "ms"); // ~3000-8000ms
        System.out.println("性能提升: " + (cost1 / cost2) + "倍");
    }
}

八、总结与最佳实践

原则做法
查询用 IN 批量findByXxxIn(List) 替代循环 findByXxx(single)
结果用 Map/Set 缓存查一次,后续 O(1) 访问
写入用 saveAll 批量收集到 List 后一次性保存
IN 不超过 1000超出则分批查询
配合 Hibernate batch_size让 saveAll 真正生成 batch SQL
批量失败要降级saveAll 异常时逐条重试定位问题
批次内去重用 Set 记录已处理的 key,防止 Excel 内重复数据

以上就是Spring Data JPA批量查询与批量写入的优化实战指南的详细内容,更多关于Spring Data JPA批量查询与写入的资料请关注脚本之家其它相关文章!

相关文章

  • 实例总结Java多线程编程的方法

    实例总结Java多线程编程的方法

    在本篇文章里我们给大家总结了Java多线程编程的方法以及相关实例代码,需要的朋友们可以学习下。
    2018-10-10
  • JAVA实现图书管理系统项目

    JAVA实现图书管理系统项目

    相信每一个学生学编程的时候,应该都会写一个小项目——图书管理系统。为什么这么说呢?我认为一个学校的氛围很大一部分可以从图书馆的氛围看出来,而图书管理系统这个不大不小的项目,接触的多,也比较熟悉,不会有陌生感,能够练手,又有些难度,所以我的小项目也来了
    2021-10-10
  • logback的AsyncAppender高效日志处理方式源码解析

    logback的AsyncAppender高效日志处理方式源码解析

    这篇文章主要为大家介绍了logback的AsyncAppender高效日志处理方式源码解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-10-10
  • ReentrantLock源码详解--条件锁

    ReentrantLock源码详解--条件锁

    这篇文章主要介绍了ReentrantLock源码之条件锁,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,下面我们来一起学习一下吧
    2019-06-06
  • ReentrantReadWriteLock不能锁升级的原因总结

    ReentrantReadWriteLock不能锁升级的原因总结

    今天给大家带来的是关于Java并发的相关知识,文章围绕着为什么ReentrantReadWriteLock不能锁升级展开,文中有非常详细的介绍及代码示例,需要的朋友可以参考下
    2021-06-06
  • Java实现员工信息管理系统

    Java实现员工信息管理系统

    这篇文章主要为大家详细介绍了Java实现员工信息管理系统,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-02-02
  • Springboot 实现Server-Sent Events的项目实践

    Springboot 实现Server-Sent Events的项目实践

    本文介绍了在Spring Boot中实现Server-Sent Events(SSE),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2024-12-12
  • SpringCloud Feign传递HttpServletRequest对象流程

    SpringCloud Feign传递HttpServletRequest对象流程

    HttpServletRequest接口的对象代表客户端的请求,当客户端通过HTTP协议访问Tomcat服务器时,HTTP请求中的所有信息都封装在HttpServletRequest接口的对象中,这篇文章介绍了Feign传递HttpServletRequest对象的流程,感兴趣的同学可以参考下文
    2023-05-05
  • 使用redisTemplate的scan方式删除批量key问题

    使用redisTemplate的scan方式删除批量key问题

    这篇文章主要介绍了使用redisTemplate的scan方式删除批量key问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-12-12
  • Java面向对象之什么是异常

    Java面向对象之什么是异常

    Java 把异常当作对象来处理,并定义一个基类,java.lang.Throwable 作为所有异常的超类。今天通过本文给大家分享Java面向对象之什么是异常,感兴趣的朋友一起看看吧
    2021-07-07

最新评论