001package org.itsallcode.jdbc.batch; 002 003import static java.util.stream.Collectors.joining; 004 005import java.util.List; 006import java.util.Objects; 007import java.util.function.Function; 008import java.util.logging.Logger; 009 010import org.itsallcode.jdbc.SimpleConnection; 011import org.itsallcode.jdbc.SimplePreparedStatement; 012import org.itsallcode.jdbc.identifier.Identifier; 013 014/** 015 * Builder for {@link PreparedStatementBatch}. Create a new builder instance 016 * using {@link SimpleConnection#preparedStatementBatch()}. 017 */ 018public class PreparedStatementBatchBuilder { 019 private static final Logger LOG = Logger.getLogger(PreparedStatementBatchBuilder.class.getName()); 020 /** Default maximum batch size. */ 021 public static final int DEFAULT_MAX_BATCH_SIZE = 200_000; 022 private final Function<String, SimplePreparedStatement> statementFactory; 023 private String sql; 024 private int maxBatchSize = DEFAULT_MAX_BATCH_SIZE; 025 026 /** 027 * Create a new instance. 028 * 029 * @param statementFactory factory for creating {@link SimplePreparedStatement}. 030 */ 031 public PreparedStatementBatchBuilder(final Function<String, SimplePreparedStatement> statementFactory) { 032 this.statementFactory = statementFactory; 033 } 034 035 /** 036 * Define the SQL statement to be used for the batch job, e.g. {@code INSERT} or 037 * {@code UPDATE}. 038 * 039 * @param sql SQL statement 040 * @return {@code this} for fluent programming 041 */ 042 public PreparedStatementBatchBuilder sql(final String sql) { 043 this.sql = sql; 044 return this; 045 } 046 047 /** 048 * Define table and column names used for generating the {@code INSERT} 049 * statement. 050 * 051 * @param tableName table name 052 * @param columnNames column names 053 * @return {@code this} for fluent programming 054 */ 055 @SuppressWarnings("java:S3242") // Using List instead of Collection to preserve column order 056 public PreparedStatementBatchBuilder into(final Identifier tableName, final List<Identifier> columnNames) { 057 this.sql = createInsertStatement(tableName, columnNames); 058 return this; 059 } 060 061 /** 062 * Define table and column names used for generating the {@code INSERT} 063 * statement. 064 * 065 * @param tableName table name 066 * @param columnNames column names 067 * @return {@code this} for fluent programming 068 */ 069 @SuppressWarnings("java:S3242") // Using List instead of Collection to preserve column order 070 public PreparedStatementBatchBuilder into(final String tableName, final List<String> columnNames) { 071 return into(Identifier.simple(tableName), columnNames.stream().map(Identifier::simple).toList()); 072 } 073 074 /** 075 * Define maximum batch size, using {@link #DEFAULT_MAX_BATCH_SIZE} as default. 076 * 077 * @param maxBatchSize maximum batch size 078 * @return {@code this} for fluent programming 079 */ 080 public PreparedStatementBatchBuilder maxBatchSize(final int maxBatchSize) { 081 this.maxBatchSize = maxBatchSize; 082 return this; 083 } 084 085 private static String createInsertStatement(final Identifier table, final List<Identifier> columnNames) { 086 final String columns = columnNames.stream().map(Identifier::quote).collect(joining(",")); 087 final String placeholders = columnNames.stream().map(n -> "?").collect(joining(",")); 088 return "insert into " + table.quote() + " (" + columns + ") values (" + placeholders + ")"; 089 } 090 091 /** 092 * Build the batch inserter. 093 * 094 * @return the batch inserter 095 */ 096 public PreparedStatementBatch build() { 097 Objects.requireNonNull(this.sql, "sql"); 098 LOG.finest(() -> "Running insert statement '" + sql + "'..."); 099 final SimplePreparedStatement statement = statementFactory.apply(sql); 100 return new PreparedStatementBatch(statement, this.maxBatchSize); 101 } 102}