From 1ce68ad87017d1cf61eea1c9dabeb1ff9715c28b Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Sun, 20 Oct 2024 16:58:55 +0200 Subject: [PATCH] Refactor parameter setting --- .gitignore | 1 + .../java/org/itsallcode/jdbc/Context.java | 10 --- .../org/itsallcode/jdbc/ParameterMapper.java | 78 ------------------- .../org/itsallcode/jdbc/SimpleConnection.java | 5 +- .../jdbc/dialect/ColumnValueSetter.java | 9 +++ .../itsallcode/jdbc/dialect/DbDialect.java | 2 + .../jdbc/dialect/ExasolDialect.java | 20 +++++ .../itsallcode/jdbc/dialect/Extractors.java | 14 ++-- .../itsallcode/jdbc/dialect/H2Dialect.java | 5 ++ .../org/itsallcode/jdbc/dialect/Setters.java | 30 +++++++ .../ConvertingPreparedStatement.java | 22 +++--- .../jdbc/statement/ParamSetterProvider.java | 25 ++++++ .../java/org/itsallcode/jdbc/H2TypeTest.java | 6 +- .../itsallcode/jdbc/ParameterMapperTest.java | 32 -------- .../jdbc/dialect/ExasolDialectTest.java | 58 ++++++++++++++ .../ConvertingPreparedStatementTest.java | 15 ++-- 16 files changed, 179 insertions(+), 153 deletions(-) delete mode 100644 src/main/java/org/itsallcode/jdbc/ParameterMapper.java create mode 100644 src/main/java/org/itsallcode/jdbc/dialect/ColumnValueSetter.java create mode 100644 src/main/java/org/itsallcode/jdbc/dialect/Setters.java create mode 100644 src/main/java/org/itsallcode/jdbc/statement/ParamSetterProvider.java delete mode 100644 src/test/java/org/itsallcode/jdbc/ParameterMapperTest.java create mode 100644 src/test/java/org/itsallcode/jdbc/dialect/ExasolDialectTest.java diff --git a/.gitignore b/.gitignore index 294191e..954785d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ /.classpath /bin/ /.settings/org.eclipse.buildship.core.prefs +/.idea/ diff --git a/src/main/java/org/itsallcode/jdbc/Context.java b/src/main/java/org/itsallcode/jdbc/Context.java index fdca903..cef2b44 100644 --- a/src/main/java/org/itsallcode/jdbc/Context.java +++ b/src/main/java/org/itsallcode/jdbc/Context.java @@ -8,16 +8,6 @@ public final class Context { private Context() { } - /** - * Get the configured {@link ParameterMapper}. - * - * @return parameter mapper - */ - @SuppressWarnings("java:S2325") // Not-static by intention - public ParameterMapper getParameterMapper() { - return ParameterMapper.create(); - } - /** * Create a new builder for {@link Context} objects. * diff --git a/src/main/java/org/itsallcode/jdbc/ParameterMapper.java b/src/main/java/org/itsallcode/jdbc/ParameterMapper.java deleted file mode 100644 index b3db6ce..0000000 --- a/src/main/java/org/itsallcode/jdbc/ParameterMapper.java +++ /dev/null @@ -1,78 +0,0 @@ -package org.itsallcode.jdbc; - -import static java.util.stream.Collectors.toMap; - -import java.time.*; -import java.time.format.DateTimeFormatter; -import java.util.*; -import java.util.function.Function; - -/** - * This class converts parameters before setting them for a prepared statement, - * e.g. in {@link PreparedStatementSetter}. - */ -public final class ParameterMapper { - private final Map> mappers; - - private ParameterMapper(final Map> mappers) { - this.mappers = new HashMap<>(mappers); - } - - /** - * Create a new mapper with predefined converters for date time types. - * - * @return a pre-configured mapper - */ - public static ParameterMapper create() { - final List> mappers = new ArrayList<>(); - final DateTimeFormatter instantFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); - final ZoneId utc = ZoneId.of("UTC"); - - mappers.add(createMapper(LocalDate.class, Object::toString)); - mappers.add(createMapper(Instant.class, - o -> instantFormatter.format(LocalDateTime.ofInstant(o, utc)))); - mappers.add(createMapper(LocalDateTime.class, instantFormatter::format)); - return new ParameterMapper(mappers.stream().collect(toMap(Mapper::getTypeName, Function.identity()))); - } - - private static Mapper createMapper(final Class type, final Function mapper) { - return new Mapper<>(type, mapper); - } - - private Optional> getMapper(final Class type) { - return Optional.ofNullable(mappers.get(type.getName())); - } - - /** - * Converts a single value. - * - * @param value value to convert - * @return converted value - */ - public Object map(final Object value) { - if (value == null) { - return null; - } - return getMapper(value.getClass()) - .map(m -> m.map(value)) - .orElse(value); - } - - private static class Mapper { - private final Class type; - private final Function mapperFunction; - - Mapper(final Class type, final Function mapper) { - this.type = type; - this.mapperFunction = mapper; - } - - Object map(final Object value) { - return mapperFunction.apply(type.cast(value)); - } - - String getTypeName() { - return type.getName(); - } - } -} diff --git a/src/main/java/org/itsallcode/jdbc/SimpleConnection.java b/src/main/java/org/itsallcode/jdbc/SimpleConnection.java index e17b2fc..7d42c1e 100644 --- a/src/main/java/org/itsallcode/jdbc/SimpleConnection.java +++ b/src/main/java/org/itsallcode/jdbc/SimpleConnection.java @@ -11,6 +11,7 @@ import org.itsallcode.jdbc.resultset.*; import org.itsallcode.jdbc.resultset.generic.Row; import org.itsallcode.jdbc.statement.ConvertingPreparedStatement; +import org.itsallcode.jdbc.statement.ParamSetterProvider; /** * A simplified version of a JDBC {@link Connection}. Create new connections @@ -22,11 +23,13 @@ public class SimpleConnection implements AutoCloseable { private final Connection connection; private final Context context; private final DbDialect dialect; + private final ParamSetterProvider paramSetterProvider; SimpleConnection(final Connection connection, final Context context, final DbDialect dialect) { this.connection = Objects.requireNonNull(connection, "connection"); this.context = Objects.requireNonNull(context, "context"); this.dialect = Objects.requireNonNull(dialect, "dialect"); + this.paramSetterProvider = new ParamSetterProvider(dialect); } /** @@ -102,7 +105,7 @@ SimplePreparedStatement prepareStatement(final String sql) { } private PreparedStatement wrap(final PreparedStatement preparedStatement) { - return new ConvertingPreparedStatement(preparedStatement, context.getParameterMapper()); + return new ConvertingPreparedStatement(preparedStatement, paramSetterProvider); } /** diff --git a/src/main/java/org/itsallcode/jdbc/dialect/ColumnValueSetter.java b/src/main/java/org/itsallcode/jdbc/dialect/ColumnValueSetter.java new file mode 100644 index 0000000..a124cbd --- /dev/null +++ b/src/main/java/org/itsallcode/jdbc/dialect/ColumnValueSetter.java @@ -0,0 +1,9 @@ +package org.itsallcode.jdbc.dialect; + +import java.sql.PreparedStatement; +import java.sql.SQLException; + +public interface ColumnValueSetter { + + void setObject(PreparedStatement stmt, int parameterIndex, T object) throws SQLException; +} diff --git a/src/main/java/org/itsallcode/jdbc/dialect/DbDialect.java b/src/main/java/org/itsallcode/jdbc/dialect/DbDialect.java index f91d09b..dfc0b7b 100644 --- a/src/main/java/org/itsallcode/jdbc/dialect/DbDialect.java +++ b/src/main/java/org/itsallcode/jdbc/dialect/DbDialect.java @@ -22,4 +22,6 @@ public interface DbDialect { * @return extractor */ ColumnValueExtractor createExtractor(final ColumnMetaData column); + + ColumnValueSetter createSetter(Class type); } diff --git a/src/main/java/org/itsallcode/jdbc/dialect/ExasolDialect.java b/src/main/java/org/itsallcode/jdbc/dialect/ExasolDialect.java index 8729764..fd1d335 100644 --- a/src/main/java/org/itsallcode/jdbc/dialect/ExasolDialect.java +++ b/src/main/java/org/itsallcode/jdbc/dialect/ExasolDialect.java @@ -1,11 +1,16 @@ package org.itsallcode.jdbc.dialect; +import java.time.*; +import java.time.format.DateTimeFormatter; + import org.itsallcode.jdbc.resultset.generic.ColumnMetaData; /** * Dialect for the Exasol database. */ public class ExasolDialect extends AbstractDbDialect { + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); + private static final ZoneId UTC_ZONE = ZoneId.of("UTC"); /** * Create a new instance. @@ -24,4 +29,19 @@ public ColumnValueExtractor createExtractor(final ColumnMetaData column) { default -> Extractors.generic(); }; } + + @SuppressWarnings("unchecked") + @Override + public ColumnValueSetter createSetter(final Class type) { + if (type == LocalDate.class) { + return (ColumnValueSetter) Setters.localDateToString(); + } + if (type == Instant.class) { + return (ColumnValueSetter) Setters.instantToString(DATE_TIME_FORMATTER, UTC_ZONE); + } + if (type == LocalDateTime.class) { + return (ColumnValueSetter) Setters.localDateTimeToString(DATE_TIME_FORMATTER); + } + return Setters.generic(); + } } diff --git a/src/main/java/org/itsallcode/jdbc/dialect/Extractors.java b/src/main/java/org/itsallcode/jdbc/dialect/Extractors.java index c5a97c1..d598544 100644 --- a/src/main/java/org/itsallcode/jdbc/dialect/Extractors.java +++ b/src/main/java/org/itsallcode/jdbc/dialect/Extractors.java @@ -6,12 +6,14 @@ final class Extractors { + @SuppressWarnings("java:S2143") // Need to use calendar api + private final static Calendar UTC_CALENDAR = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + private Extractors() { } static ColumnValueExtractor timestampToUTCInstant() { - final Calendar utcCalendar = createUtcCalendar(); - return nonNull((resultSet, columnIndex) -> resultSet.getTimestamp(columnIndex, utcCalendar).toInstant()); + return nonNull((resultSet, columnIndex) -> resultSet.getTimestamp(columnIndex, UTC_CALENDAR).toInstant()); } public static ColumnValueExtractor timestampToInstant() { @@ -19,13 +21,7 @@ public static ColumnValueExtractor timestampToInstant() { } static ColumnValueExtractor dateToLocalDate() { - final Calendar utcCalendar = createUtcCalendar(); - return nonNull((resultSet, columnIndex) -> resultSet.getDate(columnIndex, utcCalendar).toLocalDate()); - } - - @SuppressWarnings("java:S2143") // Need to use calendar api - private static Calendar createUtcCalendar() { - return Calendar.getInstance(TimeZone.getTimeZone("UTC")); + return nonNull((resultSet, columnIndex) -> resultSet.getDate(columnIndex, UTC_CALENDAR).toLocalDate()); } private static ColumnValueExtractor nonNull(final ColumnValueExtractor extractor) { diff --git a/src/main/java/org/itsallcode/jdbc/dialect/H2Dialect.java b/src/main/java/org/itsallcode/jdbc/dialect/H2Dialect.java index 390c4b1..c004c81 100644 --- a/src/main/java/org/itsallcode/jdbc/dialect/H2Dialect.java +++ b/src/main/java/org/itsallcode/jdbc/dialect/H2Dialect.java @@ -29,4 +29,9 @@ public ColumnValueExtractor createExtractor(final ColumnMetaData column) { default -> Extractors.generic(); }; } + + @Override + public ColumnValueSetter createSetter(final Class type) { + return Setters.generic(); + } } diff --git a/src/main/java/org/itsallcode/jdbc/dialect/Setters.java b/src/main/java/org/itsallcode/jdbc/dialect/Setters.java new file mode 100644 index 0000000..406217b --- /dev/null +++ b/src/main/java/org/itsallcode/jdbc/dialect/Setters.java @@ -0,0 +1,30 @@ +package org.itsallcode.jdbc.dialect; + +import java.sql.PreparedStatement; +import java.time.*; +import java.time.format.DateTimeFormatter; + +public final class Setters { + private Setters() { + } + + public static ColumnValueSetter generic() { + return PreparedStatement::setObject; + } + + static ColumnValueSetter localDateToString() { + return (final PreparedStatement stmt, final int parameterIndex, final LocalDate date) -> stmt + .setString(parameterIndex, date.toString()); + } + + public static ColumnValueSetter instantToString(final DateTimeFormatter dateTimeFormatter, + final ZoneId timeZone) { + return (final PreparedStatement stmt, final int parameterIndex, final Instant instant) -> stmt + .setString(parameterIndex, dateTimeFormatter.format(LocalDateTime.ofInstant(instant, timeZone))); + } + + public static ColumnValueSetter localDateTimeToString(final DateTimeFormatter dateTimeFormatter) { + return (final PreparedStatement stmt, final int parameterIndex, final LocalDateTime localDateTime) -> stmt + .setString(parameterIndex, dateTimeFormatter.format(localDateTime)); + } +} diff --git a/src/main/java/org/itsallcode/jdbc/statement/ConvertingPreparedStatement.java b/src/main/java/org/itsallcode/jdbc/statement/ConvertingPreparedStatement.java index 3be676b..ca3a177 100644 --- a/src/main/java/org/itsallcode/jdbc/statement/ConvertingPreparedStatement.java +++ b/src/main/java/org/itsallcode/jdbc/statement/ConvertingPreparedStatement.java @@ -3,33 +3,31 @@ import java.sql.PreparedStatement; import java.sql.SQLException; -import org.itsallcode.jdbc.ParameterMapper; /** * A {@link PreparedStatement} that delegates all methods and converts parameter * value of {@link #convert(Object)} using a {@link ParameterMapper}. */ public class ConvertingPreparedStatement extends DelegatingPreparedStatement { - - private final ParameterMapper parameterMapper; + private final PreparedStatement originalDelegate; + private final ParamSetterProvider paramSetterProvider; /** * Create a new instance. * - * @param delegate delegate - * @param parameterMapper parameter mapper + * @param delegate delegate + * @param paramSetterProvider parameter setter provider */ - public ConvertingPreparedStatement(final PreparedStatement delegate, final ParameterMapper parameterMapper) { + public ConvertingPreparedStatement(final PreparedStatement delegate, + final ParamSetterProvider paramSetterProvider) { super(delegate); - this.parameterMapper = parameterMapper; + this.originalDelegate = delegate; + this.paramSetterProvider = paramSetterProvider; } @Override public void setObject(final int parameterIndex, final Object x) throws SQLException { - super.setObject(parameterIndex, convert(x)); - } - - private Object convert(final Object object) { - return parameterMapper.map(object); + paramSetterProvider.findSetter(x) + .setObject(originalDelegate, parameterIndex, x); } } diff --git a/src/main/java/org/itsallcode/jdbc/statement/ParamSetterProvider.java b/src/main/java/org/itsallcode/jdbc/statement/ParamSetterProvider.java new file mode 100644 index 0000000..3edafd6 --- /dev/null +++ b/src/main/java/org/itsallcode/jdbc/statement/ParamSetterProvider.java @@ -0,0 +1,25 @@ +package org.itsallcode.jdbc.statement; + +import java.util.HashMap; +import java.util.Map; + +import org.itsallcode.jdbc.dialect.*; + +public class ParamSetterProvider { + private static final ColumnValueSetter GENERIC_SETTER = Setters.generic(); + private final DbDialect dialect; + private final Map, ColumnValueSetter> setters = new HashMap<>(); + + public ParamSetterProvider(final DbDialect dialect) { + this.dialect = dialect; + } + + @SuppressWarnings("unchecked") + ColumnValueSetter findSetter(final Object object) { + if (object == null) { + return GENERIC_SETTER; + } + return setters.computeIfAbsent(object.getClass(), + type -> (ColumnValueSetter) dialect.createSetter(type)); + } +} diff --git a/src/test/java/org/itsallcode/jdbc/H2TypeTest.java b/src/test/java/org/itsallcode/jdbc/H2TypeTest.java index 4e872dc..f15499b 100644 --- a/src/test/java/org/itsallcode/jdbc/H2TypeTest.java +++ b/src/test/java/org/itsallcode/jdbc/H2TypeTest.java @@ -79,10 +79,8 @@ void preparedStatementSetParameter(final TypeTest test) { try (final SimpleConnection connection = H2TestFixture.createMemConnection(); final SimpleResultSet result = connection .query("select ?", - preparedStatement -> preparedStatement.setObject(1, - value), - (resultSet, rowNum) -> resultSet - .getObject(1, value.getClass()))) { + stmt -> stmt.setObject(1, value), + (rs, rowNum) -> rs.getObject(1, value.getClass()))) { assertThat(result.toList()).containsExactly(value); } } diff --git a/src/test/java/org/itsallcode/jdbc/ParameterMapperTest.java b/src/test/java/org/itsallcode/jdbc/ParameterMapperTest.java deleted file mode 100644 index 3a23850..0000000 --- a/src/test/java/org/itsallcode/jdbc/ParameterMapperTest.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.itsallcode.jdbc; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.time.*; -import java.util.stream.Stream; - -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -class ParameterMapperTest { - - static Stream mappedTypes() { - return Stream.of(mapType(null, null), mapType("test", "test"), mapType(1, 1), mapType(1L, 1L), - mapType(1.0, 1.0), mapType(1.0f, 1.0f), mapType(true, true), mapType(false, false), - mapType(LocalDate.of(2024, 9, 1), "2024-09-01"), - mapType(Instant.parse("2007-12-03T10:15:30.00Z"), "2007-12-03 10:15:30.000"), - mapType(LocalDateTime.parse("2007-12-03T10:15:30"), "2007-12-03 10:15:30.000")); - } - - static Arguments mapType(final Object input, final Object expected) { - return Arguments.of(input, expected); - } - - @ParameterizedTest - @MethodSource("mappedTypes") - void map(final Object input, final Object expected) { - final Object actual = ParameterMapper.create().map(input); - assertThat(actual).isEqualTo(expected); - } -} diff --git a/src/test/java/org/itsallcode/jdbc/dialect/ExasolDialectTest.java b/src/test/java/org/itsallcode/jdbc/dialect/ExasolDialectTest.java new file mode 100644 index 0000000..495e0b9 --- /dev/null +++ b/src/test/java/org/itsallcode/jdbc/dialect/ExasolDialectTest.java @@ -0,0 +1,58 @@ +package org.itsallcode.jdbc.dialect; + +import static org.mockito.Mockito.verify; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.time.*; +import java.util.stream.Stream; + +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class ExasolDialectTest { + @Mock + PreparedStatement stmtMock; + + static Stream mappedTypes() { + return Stream.of(mapType("test", "test"), mapType(1, 1), mapType(1L, 1L), + mapType(1.0, 1.0), mapType(1.0f, 1.0f), mapType(true, true), mapType(false, false)); + } + + static Arguments mapType(final Object input, final Object expected) { + return Arguments.of(input, expected); + } + + @ParameterizedTest + @MethodSource("mappedTypes") + void createGenericSetter(final Object input, final Object expected) throws SQLException { + @SuppressWarnings("unchecked") + final ColumnValueSetter setter = (ColumnValueSetter) new ExasolDialect() + .createSetter(input.getClass()); + + setter.setObject(stmtMock, 0, input); + verify(stmtMock).setObject(0, expected); + } + + static Stream mappedTypesToString() { + return Stream.of(mapType(LocalDate.of(2024, 9, 1), "2024-09-01"), + mapType(Instant.parse("2007-12-03T10:15:30.00Z"), "2007-12-03 10:15:30.000"), + mapType(LocalDateTime.parse("2007-12-03T10:15:30"), "2007-12-03 10:15:30.000")); + } + + @ParameterizedTest + @MethodSource("mappedTypesToString") + void createToStringSetter(final Object input, final Object expected) throws SQLException { + @SuppressWarnings("unchecked") + final ColumnValueSetter setter = (ColumnValueSetter) new ExasolDialect() + .createSetter(input.getClass()); + + setter.setObject(stmtMock, 0, input); + verify(stmtMock).setString(0, (String) expected); + } +} diff --git a/src/test/java/org/itsallcode/jdbc/statement/ConvertingPreparedStatementTest.java b/src/test/java/org/itsallcode/jdbc/statement/ConvertingPreparedStatementTest.java index 3380f5a..6e6a0b2 100644 --- a/src/test/java/org/itsallcode/jdbc/statement/ConvertingPreparedStatementTest.java +++ b/src/test/java/org/itsallcode/jdbc/statement/ConvertingPreparedStatementTest.java @@ -8,7 +8,7 @@ import java.sql.PreparedStatement; import java.sql.SQLException; -import org.itsallcode.jdbc.ParameterMapper; +import org.itsallcode.jdbc.dialect.ColumnValueSetter; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -19,15 +19,16 @@ class ConvertingPreparedStatementTest { @Mock PreparedStatement delegateMock; @Mock - ParameterMapper parameterMapperMock; + ParamSetterProvider paramSetterProviderMock; + @Mock + ColumnValueSetter paramSetterMock; @SuppressWarnings("resource") @Test void setObject() throws SQLException { - final Object o1 = new Object(); - final Object o2 = new Object(); - when(parameterMapperMock.map(same(o1))).thenReturn(o2); - new ConvertingPreparedStatement(delegateMock, parameterMapperMock).setObject(1, o1); - verify(delegateMock).setObject(eq(1), same(o2)); + final Object o = new Object(); + when(paramSetterProviderMock.findSetter(same(o))).thenReturn(paramSetterMock); + new ConvertingPreparedStatement(delegateMock, paramSetterProviderMock).setObject(1, o); + verify(paramSetterMock).setObject(same(delegateMock), eq(1), same(o)); } }