From a450be224081ae89debd83dd6763bed3efe5324e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kraus?= Date: Tue, 25 Jul 2023 17:43:09 +0200 Subject: [PATCH] Issue #7230 - JDBC type specific setters based on EclipseLink MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomáš Kraus --- .../io/helidon/dbclient/DbClientContext.java | 55 ++++-- .../io/helidon/dbclient/DbExecuteBase.java | 11 ++ .../io/helidon/dbclient/DbExecuteContext.java | 54 ++++-- .../io/helidon/dbclient/DbStatementBase.java | 12 ++ dbclient/jdbc/pom.xml | 45 +++++ .../io/helidon/dbclient/jdbc/JdbcClient.java | 10 +- .../dbclient/jdbc/JdbcClientBuilder.java | 26 +++ .../dbclient/jdbc/JdbcClientContext.java | 78 ++++++++ .../io/helidon/dbclient/jdbc/JdbcExecute.java | 17 +- .../dbclient/jdbc/JdbcExecuteContext.java | 70 +++++++ .../jdbc/JdbcParametersConfigBlueprint.java | 95 ++++++++++ .../helidon/dbclient/jdbc/JdbcStatement.java | 177 +++++++++++++++++- .../dbclient/jdbc/JdbcStatementDml.java | 3 +- .../dbclient/jdbc/JdbcStatementGet.java | 3 +- .../dbclient/jdbc/JdbcStatementQuery.java | 3 +- .../dbclient/jdbc/JdbcTransaction.java | 13 +- .../jdbc/JdbcTransactionStatement.java | 3 +- .../jdbc/JdbcTransactionStatementDml.java | 3 +- .../jdbc/JdbcTransactionStatementGet.java | 3 +- .../jdbc/JdbcTransactionStatementQuery.java | 4 +- dbclient/jdbc/src/main/java/module-info.java | 3 +- .../dbclient/jdbc/JdbcClientBuilderTest.java | 60 ++++++ dbclient/jdbc/src/test/resources/params.yaml | 25 +++ 23 files changed, 706 insertions(+), 67 deletions(-) create mode 100644 dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcClientContext.java create mode 100644 dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcExecuteContext.java create mode 100644 dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcParametersConfigBlueprint.java create mode 100644 dbclient/jdbc/src/test/resources/params.yaml diff --git a/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbClientContext.java b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbClientContext.java index cd2a8bc76b1..c056abc4995 100644 --- a/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbClientContext.java +++ b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbClientContext.java @@ -32,7 +32,13 @@ public class DbClientContext implements DbContext { private final DbStatements statements; private final String dbType; - private DbClientContext(Builder builder) { + /** + * Create an instance of client context. + * + * @param builder Builder for {@link DbClientContext} + */ + protected DbClientContext( + BuilderBase, ? extends DbClientContext> builder) { this.dbMapperManager = builder.dbMapperManager; this.mapperManager = builder.mapperManager; this.clientServices = builder.clientServices; @@ -81,7 +87,22 @@ public static Builder builder() { /** * Builder for {@link DbClientContext}. */ - public static class Builder implements io.helidon.common.Builder { + public static final class Builder extends BuilderBase { + + @Override + public DbClientContext build() { + return new DbClientContext(this); + } + + } + + /** + * Base builder for {@link DbClientContext}. + * + * @param type of the builder + * @param type of the built instance + */ + public abstract static class BuilderBase, T extends DbClientContext> implements io.helidon.common.Builder { private DbMapperManager dbMapperManager; private MapperManager mapperManager; @@ -89,12 +110,10 @@ public static class Builder implements io.helidon.common.Builder clientServices) { + public B clientServices(List clientServices) { this.clientServices = clientServices; - return this; + return identity(); } /** @@ -136,9 +155,9 @@ public Builder clientServices(List clientServices) { * @param statements statements * @return updated builder instance */ - public Builder statements(DbStatements statements) { + public B statements(DbStatements statements) { this.statements = statements; - return this; + return identity(); } /** @@ -147,9 +166,9 @@ public Builder statements(DbStatements statements) { * @param dbType database provider type * @return updated builder instance */ - public Builder dbType(String dbType) { + public B dbType(String dbType) { this.dbType = dbType; - return this; + return identity(); } } } diff --git a/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbExecuteBase.java b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbExecuteBase.java index c02ac4e9e69..4365428c909 100644 --- a/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbExecuteBase.java +++ b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbExecuteBase.java @@ -56,6 +56,17 @@ protected DbClientContext context() { return context; } + /** + * Return database client context cast to it's extending class. + * + * @param cls {@link DbClientContext} extending class + * @return extended client context + * @param client context extending type + */ + protected C context(Class cls) { + return cls.cast(context); + } + @Override public DbStatementQuery createNamedQuery(String statementName) { return createNamedQuery(statementName, statementText(statementName)); diff --git a/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbExecuteContext.java b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbExecuteContext.java index f9102b7a19d..673da7341d9 100644 --- a/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbExecuteContext.java +++ b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbExecuteContext.java @@ -28,7 +28,13 @@ public class DbExecuteContext implements DbContext { private final String statement; private final DbClientContext clientContext; - private DbExecuteContext(Builder builder) { + /** + * Creates an instance of execution context. + * + * @param builder Helidon database client context builder + */ + protected DbExecuteContext( + BuilderBase, ? extends DbExecuteContext> builder) { this.statementName = builder.statementName; this.statement = builder.statement; this.clientContext = builder.clientContext; @@ -77,6 +83,17 @@ public String dbType() { return clientContext.dbType(); } + /** + * Returns client context cast to it's extending class. + * + * @param cls {@link DbClientContext} extending class + * @return extended client context + * @param client context extending type + */ + protected C clientContext(Class cls) { + return cls.cast(clientContext); + } + /** * Create a new execution context. * @@ -102,10 +119,27 @@ public static Builder builder() { return new Builder(); } + /** * Builder for {@link DbExecuteContext}. */ - public static class Builder implements io.helidon.common.Builder { + public static final class Builder extends BuilderBase { + + @Override + public DbExecuteContext build() { + return new DbExecuteContext(this); + } + + } + + /** + * Base builder for {@link DbExecuteContext}. + * + * @param type of the builder + * @param type of the built instance + */ + public abstract static class BuilderBase, T extends DbExecuteContext> + implements io.helidon.common.Builder { private String statementName; private String statement; @@ -117,9 +151,9 @@ public static class Builder implements io.helidon.common.Builder execution context extending type + */ + protected C context(Class cls) { + return cls.cast(context); + } + /** * Get the statement parameters. * @@ -262,4 +273,5 @@ public S addParam(String name, byte[] parameter) { protected S identity() { return (S) this; } + } diff --git a/dbclient/jdbc/pom.xml b/dbclient/jdbc/pom.xml index ad252255e45..cd3803d2a72 100644 --- a/dbclient/jdbc/pom.xml +++ b/dbclient/jdbc/pom.xml @@ -42,10 +42,28 @@ io.helidon.common.features helidon-common-features-api + + io.helidon.builder + helidon-builder-api + + + io.helidon.config + helidon-config-metadata + com.zaxxer HikariCP + + io.helidon.config + helidon-config + test + + + io.helidon.config + helidon-config-yaml + test + org.junit.jupiter junit-jupiter-api @@ -75,8 +93,35 @@ helidon-common-features-processor ${helidon.version} + + io.helidon.config + helidon-config-metadata-processor + ${helidon.version} + + + io.helidon.builder + helidon-builder-processor + ${helidon.version} + + + + io.helidon.builder + helidon-builder-processor + ${helidon.version} + + + io.helidon.config + helidon-config-metadata-processor + ${helidon.version} + + + io.helidon.inject.configdriven + helidon-inject-configdriven-processor + ${helidon.version} + + diff --git a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcClient.java b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcClient.java index bb8b7b9caf1..f91c11e062f 100644 --- a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcClient.java +++ b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcClient.java @@ -19,7 +19,6 @@ import io.helidon.dbclient.DbClient; import io.helidon.dbclient.DbClientBase; -import io.helidon.dbclient.DbClientContext; /** * Helidon DB implementation for JDBC drivers. @@ -34,12 +33,13 @@ class JdbcClient extends DbClientBase implements DbClient { * @param builder builder */ JdbcClient(JdbcClientBuilder builder) { - super(DbClientContext.builder() + super(JdbcClientContext.jdbcBuilder() .statements(builder.statements()) .dbMapperManager(builder.dbMapperManager()) .mapperManager(builder.mapperManager()) .clientServices(builder.clientServices()) .dbType(builder.connectionPool().dbType()) + .parametersSetter(builder.parametersConfig()) .build()); connectionPool = builder.connectionPool(); } @@ -59,6 +59,11 @@ public String dbType() { return connectionPool.dbType(); } + @Override + public JdbcClientContext context() { + return (JdbcClientContext) super.context(); + } + @Override public C unwrap(Class cls) { if (Connection.class.isAssignableFrom(cls)) { @@ -70,4 +75,5 @@ public C unwrap(Class cls) { throw new UnsupportedOperationException(String.format("Class %s is not supported for unwrap", cls.getName())); } } + } diff --git a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcClientBuilder.java b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcClientBuilder.java index 5be634c6e97..f46976eb717 100644 --- a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcClientBuilder.java +++ b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcClientBuilder.java @@ -29,9 +29,11 @@ public final class JdbcClientBuilder implements DbClientBuilder { private JdbcConnectionPool connectionPool; + private JdbcParametersConfigBlueprint parametersConfig; JdbcClientBuilder() { super(); + this.parametersConfig = JdbcParametersConfig.create(); } /** @@ -52,6 +54,21 @@ public DbClient doBuild() { public JdbcClientBuilder config(Config config) { super.config(config); config.get("connection").detach().map(JdbcConnectionPool::create).ifPresent(this::connectionPool); + Config parameters = config.get("parameters"); + if (parameters.exists()) { + this.parametersConfig = JdbcParametersConfig.create(parameters); + } + return this; + } + + /** + * Configure parameters setter. + * + * @param parametersConfig parameters setter configuration + * @return updated builder instance + */ + public JdbcClientBuilder parametersSetter(JdbcParametersConfig parametersConfig) { + this.parametersConfig = parametersConfig; return this; } @@ -75,4 +92,13 @@ JdbcConnectionPool connectionPool() { return connectionPool; } + /** + * Get the parameters setter configuration. + * + * @return parameters setter configuration + */ + JdbcParametersConfigBlueprint parametersConfig() { + return parametersConfig; + } + } diff --git a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcClientContext.java b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcClientContext.java new file mode 100644 index 00000000000..052e50c69f9 --- /dev/null +++ b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcClientContext.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.dbclient.jdbc; + +import io.helidon.dbclient.DbClientContext; + +/** + * Helidon JDBC database client context. + * This instance holds configuration and runtimes that are shared by any execution + * within this client runtime. + */ +class JdbcClientContext extends DbClientContext { + + private final JdbcParametersConfigBlueprint parametersConfig; + + + JdbcClientContext(Builder builder) { + super(builder); + this.parametersConfig = builder.parametersConfig; + } + + JdbcParametersConfigBlueprint parametersConfig() { + return parametersConfig; + } + + /** + * Create Helidon JDBC database client context builder. + * + * @return JDBC database client context builder + */ + static Builder jdbcBuilder() { + return new Builder(); + } + + /** + * Builder for {@link io.helidon.dbclient.DbClientContext}. + */ + static final class Builder extends DbClientContext.BuilderBase { + + private JdbcParametersConfigBlueprint parametersConfig; + + private Builder() { + super(); + this.parametersConfig = JdbcParametersConfig.create(); + } + + /** + * Configure parameters setter. + * + * @param parametersConfig parameters setter configuration + * @return updated builder instance + */ + Builder parametersSetter(JdbcParametersConfigBlueprint parametersConfig) { + this.parametersConfig = parametersConfig; + return this; + } + + @Override + public JdbcClientContext build() { + return new JdbcClientContext(this); + } + + } + +} diff --git a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcExecute.java b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcExecute.java index 616760ec227..7433543c7ee 100644 --- a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcExecute.java +++ b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcExecute.java @@ -19,7 +19,6 @@ import io.helidon.dbclient.DbClientContext; import io.helidon.dbclient.DbExecuteBase; -import io.helidon.dbclient.DbExecuteContext; import io.helidon.dbclient.DbStatementDml; import io.helidon.dbclient.DbStatementGet; import io.helidon.dbclient.DbStatementQuery; @@ -56,34 +55,38 @@ JdbcConnectionPool connectionPool() { return connectionPool; } + JdbcClientContext jdbcContext() { + return context(JdbcClientContext.class); + } + @Override public DbStatementQuery createNamedQuery(String stmtName, String stmt) { - return new JdbcStatementQuery(connectionPool, DbExecuteContext.create(stmtName, stmt, context())); + return new JdbcStatementQuery(connectionPool, JdbcExecuteContext.jdbcCreate(stmtName, stmt, jdbcContext())); } @Override public DbStatementGet createNamedGet(String stmtName, String stmt) { - return new JdbcStatementGet(connectionPool, DbExecuteContext.create(stmtName, stmt, context())); + return new JdbcStatementGet(connectionPool, JdbcExecuteContext.jdbcCreate(stmtName, stmt, jdbcContext())); } @Override public DbStatementDml createNamedDmlStatement(String stmtName, String stmt) { - return new JdbcStatementDml(connectionPool, DML, DbExecuteContext.create(stmtName, stmt, context())); + return new JdbcStatementDml(connectionPool, DML, JdbcExecuteContext.jdbcCreate(stmtName, stmt, jdbcContext())); } @Override public DbStatementDml createNamedInsert(String stmtName, String stmt) { - return new JdbcStatementDml(connectionPool, INSERT, DbExecuteContext.create(stmtName, stmt, context())); + return new JdbcStatementDml(connectionPool, INSERT, JdbcExecuteContext.jdbcCreate(stmtName, stmt, jdbcContext())); } @Override public DbStatementDml createNamedUpdate(String stmtName, String stmt) { - return new JdbcStatementDml(connectionPool, UPDATE, DbExecuteContext.create(stmtName, stmt, context())); + return new JdbcStatementDml(connectionPool, UPDATE, JdbcExecuteContext.jdbcCreate(stmtName, stmt, jdbcContext())); } @Override public DbStatementDml createNamedDelete(String stmtName, String stmt) { - return new JdbcStatementDml(connectionPool, DELETE, DbExecuteContext.create(stmtName, stmt, context())); + return new JdbcStatementDml(connectionPool, DELETE, JdbcExecuteContext.jdbcCreate(stmtName, stmt, jdbcContext())); } @Override diff --git a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcExecuteContext.java b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcExecuteContext.java new file mode 100644 index 00000000000..03bc513405e --- /dev/null +++ b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcExecuteContext.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.dbclient.jdbc; + +import io.helidon.dbclient.DbExecuteContext; + +/** + * JDBC execution context. + */ +class JdbcExecuteContext extends DbExecuteContext { + + private JdbcExecuteContext(Builder builder) { + super(builder); + } + + JdbcParametersConfigBlueprint parametersConfig() { + return clientContext(JdbcClientContext.class).parametersConfig(); + } + + /** + * Create a new execution context. + * + * @param statementName statement name + * @param statement statement + * @param context client context + * @return execution context + */ + public static JdbcExecuteContext jdbcCreate(String statementName, String statement, JdbcClientContext context) { + return jdbcBuilder() + .statement(statement) + .statementName(statementName) + .clientContext(context) + .build(); + } + + /** + * Create Helidon JDBC database execution context builder. + * + * @return database client context builder + */ + public static Builder jdbcBuilder() { + return new Builder(); + } + + /** + * Builder for {@link DbExecuteContext}. + */ + public static final class Builder extends BuilderBase { + + @Override + public JdbcExecuteContext build() { + return new JdbcExecuteContext(this); + } + + } + +} diff --git a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcParametersConfigBlueprint.java b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcParametersConfigBlueprint.java new file mode 100644 index 00000000000..698a9be1b1f --- /dev/null +++ b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcParametersConfigBlueprint.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.dbclient.jdbc; + +import io.helidon.builder.api.Prototype; +import io.helidon.config.metadata.Configured; +import io.helidon.config.metadata.ConfiguredOption; + +/** + * JDBC parameters setter configuration. + */ +@Prototype.Blueprint +@Configured(prefix = "parameters") +interface JdbcParametersConfigBlueprint { + + /** + * Use SQL {@code NCHAR}, {@code NVARCHAR} or {@code LONGNVARCHAR} value conversion + * for {@link String} values. + * Default value is {@code false}. + * + * @return whether N{@link String} conversion is used + */ + @ConfiguredOption("false") + boolean useNString(); + + /** + * Use {@link java.sql.PreparedStatement#setCharacterStream(int, java.io.Reader, int)} binding + * for {@link String} values with length above {@link #stringBindingSize()} limit. + * Default value is {@code true}. + * + * @return whether to use {@link java.io.CharArrayReader} binding + */ + @ConfiguredOption("true") + boolean useStringBinding(); + + /** + * {@link String} values with length above this limit will be bound + * using {@link java.sql.PreparedStatement#setCharacterStream(int, java.io.Reader, int)} + * if {@link #useStringBinding()} is set to {@code true}. + * Default value is {@code 1024}. + * + * @return {@link String} values length limit for {@link java.io.CharArrayReader} binding + */ + @ConfiguredOption("1024") + int stringBindingSize(); + + /** + * Use {@link java.sql.PreparedStatement#setBinaryStream(int, java.io.InputStream, int)} binding + * for {@code byte[]} values. + * Default value is {@code true}. + * + * @return whether to use {@link java.io.ByteArrayInputStream} binding + */ + @ConfiguredOption("true") + boolean useByteArrayBinding(); + + /** + * Use {@link java.sql.PreparedStatement#setTimestamp(int, java.sql.Timestamp)} + * to set {@link java.time.LocalTime} values when {@code true} + * or use {@link java.sql.PreparedStatement#setTime(int, java.sql.Time)} when {@code false}. + * Default value is {@code true}. + *

This option is vendor specific. Most of the databases are fine with {@link java.sql.Timestamp}, + * but for example SQL Server requires {@link java.sql.Time}. + * This option does not apply when {@link #setObjectForJavaTime()} is set to {@code true}. + * + * @return whether to use {@link java.sql.Timestamp} instead of {@link java.sql.Time} + * for {@link java.time.LocalTime} values + */ + @ConfiguredOption("true") + boolean timestampForLocalTime(); + + /** + * Set all {@code java.time} Date/Time values directly using {@link java.sql.PreparedStatement#setObject(int, Object)}. + * This option shall work fine for recent JDBC drivers. + * Default value is {@code true}. + * + * @return whether to use {@link java.sql.PreparedStatement#setObject(int, Object)} for {@code java.time} Date/Time values + */ + @ConfiguredOption("true") + boolean setObjectForJavaTime(); + +} diff --git a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatement.java b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatement.java index 9e7e9ee5002..4a437e76332 100644 --- a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatement.java +++ b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatement.java @@ -15,17 +15,24 @@ */ package io.helidon.dbclient.jdbc; +import java.io.ByteArrayInputStream; +import java.io.CharArrayReader; import java.lang.System.Logger.Level; +import java.math.BigDecimal; +import java.math.BigInteger; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; +import java.sql.SQLXML; +import java.sql.Types; import java.util.ArrayList; +import java.util.Calendar; import java.util.List; import java.util.Map; +import java.util.UUID; import io.helidon.dbclient.DbClientException; import io.helidon.dbclient.DbClientServiceContext; -import io.helidon.dbclient.DbExecuteContext; import io.helidon.dbclient.DbIndexedStatementParameters; import io.helidon.dbclient.DbNamedStatementParameters; import io.helidon.dbclient.DbStatement; @@ -50,7 +57,7 @@ public abstract class JdbcStatement> extends DbStatemen * @param connectionPool connection pool * @param context context */ - JdbcStatement(JdbcConnectionPool connectionPool, DbExecuteContext context) { + JdbcStatement(JdbcConnectionPool connectionPool, JdbcExecuteContext context) { super(context); this.connectionPool = connectionPool; } @@ -68,6 +75,10 @@ void closeConnection() { } } + private JdbcExecuteContext jdbcContext() { + return context(JdbcExecuteContext.class); + } + /** * Create the {@link PreparedStatement}. * @@ -127,12 +138,12 @@ private PreparedStatement prepareNamedStatement(String stmtName, String stmt, Ma preparedStatement = prepareStatement(stmtName, convertedStmt); List namesOrder = parser.namesOrder(); // Set parameters into prepared statement - int i = 1; + int i = 1; // JDBC set position parameter starts from 1. for (String name : namesOrder) { if (parameters.containsKey(name)) { Object value = parameters.get(name); LOGGER.log(Level.TRACE, String.format("Mapped parameter %d: %s -> %s", i, name, value)); - preparedStatement.setObject(i, value); + setParameter(preparedStatement, i, value); i++; } else { throw new DbClientException(namedStatementErrorMessage(namesOrder, parameters)); @@ -152,8 +163,7 @@ private PreparedStatement prepareIndexedStatement(String stmtName, String stmt, int i = 1; // JDBC set position parameter starts from 1. for (Object value : parameters) { LOGGER.log(Level.TRACE, String.format("Indexed parameter %d: %s", i, value)); - preparedStatement.setObject(i, value); - // increase value for next iteration + setParameter(preparedStatement, i, value); i++; } return preparedStatement; @@ -194,4 +204,159 @@ private static String namedStatementErrorMessage(List names, Map jdbcContext().parametersConfig().stringBindingSize())) { + CharArrayReader reader = new CharArrayReader(s.toCharArray()); + statement.setCharacterStream(index, reader, (s.length())); + } else { + if (jdbcContext().parametersConfig().useNString()) { + statement.setNString(index, s); + } else { + statement.setString(index, s); + } + } + } else if (parameter instanceof Number number) { + if (number instanceof Integer i) { + statement.setInt(index, i); + } else if (number instanceof Long l) { + statement.setLong(index, l); + } else if (number instanceof BigDecimal bd) { + statement.setBigDecimal(index, bd); + } else if (number instanceof Double d) { + statement.setDouble(index, d); + } else if (number instanceof Float f) { + statement.setFloat(index, f); + } else if (number instanceof Short s) { + statement.setShort(index, s); + } else if (number instanceof Byte b) { + statement.setByte(index, b); + } else if (number instanceof BigInteger bi) { + // Convert to BigDecimal. + statement.setBigDecimal(index, new BigDecimal(bi)); + } else { + statement.setObject(index, number); + } + // java.sql Date/Time + } else if (parameter instanceof java.sql.Date d) { + statement.setDate(index, d); + } else if (parameter instanceof java.sql.Time t){ + statement.setTime(index, t); + } else if (parameter instanceof java.sql.Timestamp ts) { + statement.setTimestamp(index, ts); + // java.time Date/Time + } else if (parameter instanceof java.time.LocalDate ld) { + if (jdbcContext().parametersConfig().setObjectForJavaTime()) { + statement.setObject(index, ld); + } else { + statement.setDate(index, java.sql.Date.valueOf(ld)); + } + } else if (parameter instanceof java.time.LocalDateTime ldt) { + if (jdbcContext().parametersConfig().setObjectForJavaTime()) { + statement.setObject(index, ldt); + } else { + statement.setTimestamp(index, java.sql.Timestamp.valueOf(ldt)); + } + } else if (parameter instanceof java.time.OffsetDateTime odt) { + if (jdbcContext().parametersConfig().setObjectForJavaTime()) { + statement.setObject(index, odt); + } else { + statement.setTimestamp(index, java.sql.Timestamp.from((odt).toInstant())); + } + } else if (parameter instanceof java.time.LocalTime lt) { + if (jdbcContext().parametersConfig().setObjectForJavaTime()) { + statement.setObject(index, lt); + } else { + // Fallback option for old JDBC drivers may differ + if (jdbcContext().parametersConfig().timestampForLocalTime()) { + statement.setTimestamp(index, + java.sql.Timestamp.valueOf( + java.time.LocalDateTime.of(java.time.LocalDate.ofEpochDay(0), lt))); + } else { + statement.setTime(index, java.sql.Time.valueOf(lt)); + } + } + } else if (parameter instanceof java.time.OffsetTime ot) { + if (jdbcContext().parametersConfig().setObjectForJavaTime()) { + statement.setObject(index, ot); + } else { + statement.setTimestamp(index, + java.sql.Timestamp.valueOf( + java.time.LocalDateTime.of(java.time.LocalDate.ofEpochDay(0), ot.toLocalTime()))); + } + } else if (parameter instanceof Boolean b) { + statement.setBoolean(index, b); + } else if (parameter == null) { + // Normally null is passed as a DatabaseField so the type is included, but in some case may be passed directly. + statement.setNull(index, Types.VARCHAR); + } else if (parameter instanceof byte[] b) { + if (jdbcContext().parametersConfig().useByteArrayBinding()) { + ByteArrayInputStream inputStream = new ByteArrayInputStream(b); + statement.setBinaryStream(index, inputStream, b.length); + } else { + statement.setBytes(index, b); + } + // Next process types that need conversion. + } else if (parameter instanceof Calendar c) { + statement.setTimestamp(index, timestampFromDate(c.getTime())); + } else if (parameter instanceof java.util.Date d) { + statement.setTimestamp(index, timestampFromDate(d)); + } else if (parameter instanceof Character c) { + statement.setString(index, String.valueOf(c)); + } else if (parameter instanceof char[] c) { + statement.setString(index, new String(c)); + } else if (parameter instanceof Character[] c) { + statement.setString(index, String.valueOf(characterArrayToCharArray(c))); + } else if (parameter instanceof Byte[] b) { + statement.setBytes(index, byteArrayToByteArray(b)); + } else if (parameter instanceof SQLXML s) { + statement.setSQLXML(index, s); + } else if (parameter instanceof UUID uuid) { + statement.setString(index, uuid.toString()); + } else { + statement.setObject(index, parameter); + } + } + + private static java.sql.Timestamp timestampFromLong(long millis) { + java.sql.Timestamp timestamp = new java.sql.Timestamp(millis); + + // Must account for negative millis < 1970 + // Must account for the jdk millis bug where it does not set the nanos. + if ((millis % 1000) > 0) { + timestamp.setNanos((int) (millis % 1000) * 1000000); + } else if ((millis % 1000) < 0) { + timestamp.setNanos((int) (1000000000 - (Math.abs((millis % 1000) * 1000000)))); + } + return timestamp; + } + + private static java.sql.Timestamp timestampFromDate(java.util.Date date) { + return timestampFromLong(date.getTime()); + } + + private static char[] characterArrayToCharArray(Character[] source) { + char[] chars = new char[source.length]; + for (int i = 0; i < source.length; i++) { + chars[i] = source[i]; + } + return chars; + } + + private static byte[] byteArrayToByteArray(Byte[] source) { + byte[] bytes = new byte[source.length]; + for (int i = 0; i < source.length; i++) { + Byte value = source[i]; + if (value != null) { + bytes[i] = value; + } + } + return bytes; + } + } diff --git a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatementDml.java b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatementDml.java index 9cacef6e924..65bd3b94e45 100644 --- a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatementDml.java +++ b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatementDml.java @@ -20,7 +20,6 @@ import java.util.concurrent.CompletableFuture; import io.helidon.dbclient.DbClientServiceContext; -import io.helidon.dbclient.DbExecuteContext; import io.helidon.dbclient.DbStatementDml; import io.helidon.dbclient.DbStatementException; import io.helidon.dbclient.DbStatementType; @@ -38,7 +37,7 @@ class JdbcStatementDml extends JdbcStatement implements DbStatem * @param connectionPool connection pool * @param context execution context */ - JdbcStatementDml(JdbcConnectionPool connectionPool, DbStatementType type, DbExecuteContext context) { + JdbcStatementDml(JdbcConnectionPool connectionPool, DbStatementType type, JdbcExecuteContext context) { super(connectionPool, context); this.type = type; } diff --git a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatementGet.java b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatementGet.java index b5ee137e921..7fc11739ec7 100644 --- a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatementGet.java +++ b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatementGet.java @@ -22,7 +22,6 @@ import java.util.concurrent.CompletableFuture; import io.helidon.dbclient.DbClientServiceContext; -import io.helidon.dbclient.DbExecuteContext; import io.helidon.dbclient.DbRow; import io.helidon.dbclient.DbStatementException; import io.helidon.dbclient.DbStatementGet; @@ -39,7 +38,7 @@ class JdbcStatementGet extends JdbcStatement implements DbStatem * @param connectionPool connection pool * @param context execution context */ - JdbcStatementGet(JdbcConnectionPool connectionPool, DbExecuteContext context) { + JdbcStatementGet(JdbcConnectionPool connectionPool, JdbcExecuteContext context) { super(connectionPool, context); } diff --git a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatementQuery.java b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatementQuery.java index 3b87a78e379..3938f476c42 100644 --- a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatementQuery.java +++ b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatementQuery.java @@ -23,7 +23,6 @@ import java.util.stream.StreamSupport; import io.helidon.dbclient.DbClientServiceContext; -import io.helidon.dbclient.DbExecuteContext; import io.helidon.dbclient.DbRow; import io.helidon.dbclient.DbStatementDml; import io.helidon.dbclient.DbStatementException; @@ -41,7 +40,7 @@ class JdbcStatementQuery extends JdbcStatement implements DbSt * @param connectionPool connection pool * @param context context */ - JdbcStatementQuery(JdbcConnectionPool connectionPool, DbExecuteContext context) { + JdbcStatementQuery(JdbcConnectionPool connectionPool, JdbcExecuteContext context) { super(connectionPool, context); } diff --git a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcTransaction.java b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcTransaction.java index 838e8c7be5b..2acf2b04caf 100644 --- a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcTransaction.java +++ b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcTransaction.java @@ -19,7 +19,6 @@ import io.helidon.dbclient.DbClientContext; import io.helidon.dbclient.DbClientException; -import io.helidon.dbclient.DbExecuteContext; import io.helidon.dbclient.DbStatementDml; import io.helidon.dbclient.DbStatementGet; import io.helidon.dbclient.DbStatementQuery; @@ -52,7 +51,7 @@ class JdbcTransaction extends JdbcExecute implements DbTransaction { public DbStatementQuery createNamedQuery(String stmtName, String stmt) { return new JdbcTransactionStatementQuery( connectionPool(), - DbExecuteContext.create(stmtName, stmt, context()), + JdbcExecuteContext.jdbcCreate(stmtName, stmt, jdbcContext()), transactionContext); } @@ -60,7 +59,7 @@ public DbStatementQuery createNamedQuery(String stmtName, String stmt) { public DbStatementGet createNamedGet(String statementName, String statement) { return new JdbcTransactionStatementGet( connectionPool(), - DbExecuteContext.create(statementName, statement, context()), + JdbcExecuteContext.jdbcCreate(statementName, statement, jdbcContext()), transactionContext); } @@ -68,7 +67,7 @@ public DbStatementGet createNamedGet(String statementName, String statement) { public DbStatementDml createNamedDmlStatement(String statementName, String statement) { return new JdbcTransactionStatementDml( connectionPool(), - DbExecuteContext.create(statementName, statement, context()), + JdbcExecuteContext.jdbcCreate(statementName, statement, jdbcContext()), transactionContext, DML); } @@ -77,7 +76,7 @@ public DbStatementDml createNamedDmlStatement(String statementName, String state public DbStatementDml createNamedInsert(String statementName, String statement) { return new JdbcTransactionStatementDml( connectionPool(), - DbExecuteContext.create(statementName, statement, context()), + JdbcExecuteContext.jdbcCreate(statementName, statement, jdbcContext()), transactionContext, INSERT); } @@ -86,7 +85,7 @@ public DbStatementDml createNamedInsert(String statementName, String statement) public DbStatementDml createNamedUpdate(String stmtName, String stmt) { return new JdbcTransactionStatementDml( connectionPool(), - DbExecuteContext.create(stmtName, stmt, context()), + JdbcExecuteContext.jdbcCreate(stmtName, stmt, jdbcContext()), transactionContext, UPDATE); } @@ -95,7 +94,7 @@ public DbStatementDml createNamedUpdate(String stmtName, String stmt) { public DbStatementDml createNamedDelete(String statementName, String statement) { return new JdbcTransactionStatementDml( connectionPool(), - DbExecuteContext.create(statementName, statement, context()), + JdbcExecuteContext.jdbcCreate(statementName, statement, jdbcContext()), transactionContext, DELETE); } diff --git a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcTransactionStatement.java b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcTransactionStatement.java index 07865268856..9ff92fcefc3 100644 --- a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcTransactionStatement.java +++ b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcTransactionStatement.java @@ -17,7 +17,6 @@ import java.sql.PreparedStatement; -import io.helidon.dbclient.DbExecuteContext; import io.helidon.dbclient.DbStatement; /** @@ -37,7 +36,7 @@ abstract class JdbcTransactionStatement> extends JdbcSt * @param transactionContext transaction context */ protected JdbcTransactionStatement(JdbcConnectionPool connectionPool, - DbExecuteContext context, + JdbcExecuteContext context, TransactionContext transactionContext) { super(connectionPool, context); this.transactionContext = transactionContext; diff --git a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcTransactionStatementDml.java b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcTransactionStatementDml.java index d99fb0545f3..9f33049e4fd 100644 --- a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcTransactionStatementDml.java +++ b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcTransactionStatementDml.java @@ -15,7 +15,6 @@ */ package io.helidon.dbclient.jdbc; -import io.helidon.dbclient.DbExecuteContext; import io.helidon.dbclient.DbStatementDml; import io.helidon.dbclient.DbStatementType; @@ -34,7 +33,7 @@ class JdbcTransactionStatementDml extends JdbcTransactionStatement execute() { return doExecute((future, context) -> JdbcStatementQuery.doExecute(this, future, context, null)); } + } diff --git a/dbclient/jdbc/src/main/java/module-info.java b/dbclient/jdbc/src/main/java/module-info.java index d1fe0c19d56..94ba3d0f32d 100644 --- a/dbclient/jdbc/src/main/java/module-info.java +++ b/dbclient/jdbc/src/main/java/module-info.java @@ -19,7 +19,6 @@ import io.helidon.dbclient.jdbc.JdbcClientProvider; import io.helidon.dbclient.jdbc.spi.HikariCpExtensionProvider; import io.helidon.dbclient.spi.DbClientProvider; -import io.helidon.dbclient.jdbc.spi.HikariCpExtensionProvider; /** * Helidon Database Client JDBC. @@ -38,6 +37,8 @@ requires transitive io.helidon.common; requires transitive io.helidon.dbclient; + requires transitive io.helidon.builder.api; + requires transitive io.helidon.config.metadata; exports io.helidon.dbclient.jdbc; exports io.helidon.dbclient.jdbc.spi; diff --git a/dbclient/jdbc/src/test/java/io/helidon/dbclient/jdbc/JdbcClientBuilderTest.java b/dbclient/jdbc/src/test/java/io/helidon/dbclient/jdbc/JdbcClientBuilderTest.java index 7b7eb6f678f..bd212ad80c5 100644 --- a/dbclient/jdbc/src/test/java/io/helidon/dbclient/jdbc/JdbcClientBuilderTest.java +++ b/dbclient/jdbc/src/test/java/io/helidon/dbclient/jdbc/JdbcClientBuilderTest.java @@ -17,6 +17,8 @@ import java.util.List; +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; import io.helidon.dbclient.DbClient; import io.helidon.dbclient.DbClientContext; import io.helidon.dbclient.DbClientService; @@ -51,4 +53,62 @@ void testDbClientBuildWithService() { assertThat(serviceContext.dbType(), is(TEST_SERVICE_CONTEXT.dbType())); } + // Check default JDBC parameters setter configuration + @Test + void testDefaultParametersSetterConfig() { + DbClient dbClient = new JdbcClientBuilder() + .addService(context -> TEST_SERVICE_CONTEXT) + .connectionPool(() -> null) + .build(); + JdbcClientContext clientContext = dbClient.unwrap(JdbcClient.class).context(); + assertThat(clientContext.parametersConfig().useNString(), is(false)); + assertThat(clientContext.parametersConfig().useStringBinding(), is(true)); + assertThat(clientContext.parametersConfig().stringBindingSize(), is(1024)); + assertThat(clientContext.parametersConfig().useByteArrayBinding(), is(true)); + assertThat(clientContext.parametersConfig().timestampForLocalTime(), is(true)); + assertThat(clientContext.parametersConfig().setObjectForJavaTime(), is(true)); + } + + // Check custom JDBC parameters setter configuration from builder + @Test + void testCustomParametersSetterConfig() { + DbClient dbClient = new JdbcClientBuilder() + .addService(context -> TEST_SERVICE_CONTEXT) + .connectionPool(() -> null) + .parametersSetter(JdbcParametersConfig.builder() + .useNString(true) + .useStringBinding(false) + .stringBindingSize(4096) + .useByteArrayBinding(false) + .timestampForLocalTime(false) + .setObjectForJavaTime(false) + .build()) + .build(); + JdbcClientContext clientContext = dbClient.unwrap(JdbcClient.class).context(); + assertThat(clientContext.parametersConfig().useNString(), is(true)); + assertThat(clientContext.parametersConfig().useStringBinding(), is(false)); + assertThat(clientContext.parametersConfig().stringBindingSize(), is(4096)); + assertThat(clientContext.parametersConfig().useByteArrayBinding(), is(false)); + assertThat(clientContext.parametersConfig().timestampForLocalTime(), is(false)); + assertThat(clientContext.parametersConfig().setObjectForJavaTime(), is(false)); + } + + // Check custom JDBC parameters setter configuration from fonfig file + @Test + void testCustomFileParametersSetterConfig() { + Config config = Config.create(ConfigSources.classpath("params.yaml")); + DbClient dbClient = new JdbcClientBuilder() + .config(config.get("db")) + .addService(context -> TEST_SERVICE_CONTEXT) + .connectionPool(() -> null) + .build(); + JdbcClientContext clientContext = dbClient.unwrap(JdbcClient.class).context(); + assertThat(clientContext.parametersConfig().useNString(), is(true)); + assertThat(clientContext.parametersConfig().useStringBinding(), is(false)); + assertThat(clientContext.parametersConfig().stringBindingSize(), is(8192)); + assertThat(clientContext.parametersConfig().useByteArrayBinding(), is(false)); + assertThat(clientContext.parametersConfig().timestampForLocalTime(), is(false)); + assertThat(clientContext.parametersConfig().setObjectForJavaTime(), is(false)); + } + } diff --git a/dbclient/jdbc/src/test/resources/params.yaml b/dbclient/jdbc/src/test/resources/params.yaml new file mode 100644 index 00000000000..4f13a8fe2d0 --- /dev/null +++ b/dbclient/jdbc/src/test/resources/params.yaml @@ -0,0 +1,25 @@ +# +# Copyright (c) 2023 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +db: + source: jdbc + parameters: + use-n-string: true + use-string-binding: false + string-binding-size: 8192 + use-byte-array-binding: false + timestamp-for-local-time: false + set-object-for-java-time: false