首页 > 解决方案 > 持久化数千个实体的最有效方法是什么?

问题描述

我有相当大的 CSV 文件,我需要对其进行解析然后保存到 PostgreSQL 中。例如,一个文件包含 2_070_000 条记录,我能够在大约 8 分钟内解析并保留这些记录(单线程)。是否可以使用多个线程来持久化它们?

    public void importCsv(MultipartFile csvFile, Class<T> targetClass) {
        final var headerMapping = getHeaderMapping(targetClass);
        File tempFile = null;

        try {
            final var randomUuid = UUID.randomUUID().toString();
            tempFile = File.createTempFile("data-" + randomUuid, "csv");
            csvFile.transferTo(tempFile);

            final var csvFileName = csvFile.getOriginalFilename();
            final var csvReader = new BufferedReader(new FileReader(tempFile, StandardCharsets.UTF_8));

            Stopwatch stopWatch = Stopwatch.createStarted();
            log.info("Starting to import {}", csvFileName);
            final var csvRecords = CSVFormat.DEFAULT
                    .withDelimiter(';')
                    .withHeader(headerMapping.keySet().toArray(String[]::new))
                    .withSkipHeaderRecord(true)
                    .parse(csvReader);

            final var models = StreamSupport.stream(csvRecords.spliterator(), true)
                    .map(record -> parseRecord(record, headerMapping, targetClass))
                    .collect(Collectors.toUnmodifiableList());

           // How to save such a large list? 

            log.info("Finished import of {} in {}", csvFileName, stopWatch);
        } catch (IOException ex) {
            ex.printStackTrace();
        } finally {
            tempFile.delete();
        }
    }

模型包含很多记录。解析成记录是使用并行流完成的,所以速度非常快。我害怕调用SimpleJpaRepository.saveAll,因为我不确定它会在幕后做什么。

问题是:持久化如此庞大的实体列表最有效的方法是什么?

PS:非常感谢任何其他改进。

标签: javapostgresqlspring-boothibernatespring-data-jpa

解决方案


您必须使用批量插入。

  1. 为自定义存储库创建接口SomeRepositoryCustom
public interface SomeRepositoryCustom {

    void batchSave(List<Record> records);

}
  1. 创建一个实现SomeRepositoryCustom
@Repository
class SomesRepositoryCustomImpl implements SomeRepositoryCustom {

    private JdbcTemplate template;

    @Autowired
    public SomesRepositoryCustomImpl(JdbcTemplate template) {
        this.template = template;
    }

    @Override
    public void batchSave(List<Record> records) {
        final String sql = "INSERT INTO RECORDS(column_a, column_b) VALUES (?, ?)";

        template.execute(sql, (PreparedStatementCallback<Void>) ps -> {
            for (Record record : records) {
                ps.setString(1, record.getA());
                ps.setString(2, record.getB());
                ps.addBatch();
            }
            ps.executeBatch();
            return null;
        });
    }

}
  1. 扩展你JpaRepositorySomeRepositoryCustom
@Repository
public interface SomeRepository extends JpaRepository, SomeRepositoryCustom {

}

保存

someRepository.batchSave(records);

笔记

请记住,即使您使用批量插入,数据库驱动程序也不会使用它们。例如,对于 MySQL,需要在rewriteBatchedStatements=true数据库 URL 中添加一个参数。所以最好启用驱动程序 SQL 日志记录(不是 Hibernate)来验证一切。也可用于调试驱动程序代码。

您需要决定在循环中按数据包拆分记录

    for (Record record : records) { 

    }

司机可以为您完成,因此您不需要它。但最好也调试这个东西。

PS 不要var到处使用。


推荐阅读