excel-import v0.1.0
Stream `.xlsx` into PostgreSQL — memory stays flat, errors come back color-coded.
Features
Streaming SAX reader
Heap usage is independent of row count — import files that don’t fit in memory.
Annotation mapping
Map rows to POJOs with @ExcelSheet / @ExcelColumn / @TargetTable.
Jakarta Bean Validation
Per-row validation with @NotNull, @Size, @PastOrPresent and more.
Multi-row INSERT batching
One INSERT per batch, one transaction per batch, recursive bisection on failure.
Color-coded Excel report
Inserted rows green, rejected rows red, reason in an added column.
ON CONFLICT + Spring Boot starter
Conflict strategies (doNothing/doUpdate) and an optional Spring Boot autoconfigure.
How it works
The file is read twice. Pass 1 streams data through mapping, validation and batched insert; between passes only a compact outcome map lives in memory (~1 MB per million rows; messages spill to a temp file). Pass 2 re-reads the file and writes a color-coded report via SXSSF.
Install
// 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>Quickstart
@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"));