excel-import v0.1.0
Потоковый импорт `.xlsx` в PostgreSQL — память не растёт от числа строк, ошибки возвращаются с цветовой разметкой.
Возможности
Потоковое SAX-чтение
Потребление heap не зависит от числа строк — импортируются файлы, не помещающиеся в память.
Маппинг по аннотациям
Строки на POJO через @ExcelSheet / @ExcelColumn / @TargetTable.
Jakarta Bean Validation
Построчная валидация: @NotNull, @Size, @PastOrPresent и др.
Батчинг multi-row INSERT
Один INSERT на батч, одна транзакция на батч, рекурсивная бисекция при сбое.
Excel-отчёт с разметкой
Вставленные строки зелёные, отклонённые красные, причина — в добавленной колонке.
ON CONFLICT + Spring Boot starter
Стратегии конфликтов (doNothing/doUpdate) и опциональная Spring Boot autoconfiguration.
Как это работает
Файл читается дважды. Проход 1 стримит данные через маппинг, валидацию и батч-вставку; между проходами в памяти живёт только компактная карта исходов (~1 МБ на миллион строк; сообщения вытесняются во временный файл). Проход 2 перечитывает файл и пишет цветной отчёт через SXSSF.
Установка
// Gradle (Kotlin DSL)implementation("org.novgorodtsev.excelimport:excel-import-core:0.1.0")
// Spring Boot starter (core pulled transitively)implementation("org.novgorodtsev.excelimport:excel-import-spring-boot-starter:0.1.0")<!-- Maven --><dependency> <groupId>org.novgorodtsev.excelimport</groupId> <artifactId>excel-import-core</artifactId> <version>0.1.0</version></dependency><dependency> <groupId>org.novgorodtsev.excelimport</groupId> <artifactId>excel-import-spring-boot-starter</artifactId> <version>0.1.0</version></dependency>Быстрый старт
@ExcelSheet(name = "Employees", headerRow = 0)@TargetTable(schema = "hr", name = "employee")public class EmployeeRow { @ExcelColumn(header = "Personnel No") @Column("personnel_no") @NotNull private Long personnelNo;
@ExcelColumn(header = "Full Name") @Column("full_name") @NotBlank @Size(max = 200) private String fullName;
@ExcelColumn(header = "Hired At", formats = {"dd.MM.yyyy", "yyyy-MM-dd"}) @Column("hired_at") @PastOrPresent private LocalDate hiredAt;
@ExcelColumn(header = "Salary") @Column("salary") @DecimalMin("0.00") private BigDecimal salary;
@ExcelColumn(header = "Department", required = false) @Column("department") private String department; // getters/setters}
ImportConfig config = ImportConfig.builder() .batchSize(1_000) .conflictStrategy(ConflictStrategy.doNothing("personnel_no")) .reportPath(Path.of("/var/reports/employees-report.xlsx")) .maxErrors(10_000) .build();
ExcelImporter<EmployeeRow> importer = ExcelImporter.builder(EmployeeRow.class) .dataSource(dataSource) .config(config) .build();
ImportReport report = importer.importFile(Path.of("employees.xlsx"));