From 019c34f4803f357ac2d522446a806f823c207555 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 25 Jul 2023 18:10:30 +0200 Subject: [PATCH] Introduce JdbcClient as a fluent facade for query/update execution Delegates to JdbcTemplate/NamedParameterJdbcTemplate underneath the covers. Supports parameter objects/records through SimplePropertySqlParameterSource. Closes gh-30931 --- .../jdbc/core/JdbcOperations.java | 13 +- .../jdbc/core/JdbcTemplate.java | 14 +- .../BeanPropertySqlParameterSource.java | 9 +- .../namedparam/MapSqlParameterSource.java | 8 + .../NamedParameterJdbcOperations.java | 8 +- .../NamedParameterJdbcTemplate.java | 15 +- .../SimplePropertySqlParameterSource.java | 117 +++++ .../jdbc/core/simple/DefaultJdbcClient.java | 313 ++++++++++++ .../jdbc/core/simple/JdbcClient.java | 366 ++++++++++++++ .../namedparam/NamedParameterQueryTests.java | 58 ++- .../JdbcClientIndexedParameterTests.java | 332 ++++++++++++ .../simple/JdbcClientNamedParameterTests.java | 338 +++++++++++++ .../core/simple/JdbcClientQueryTests.java | 476 ++++++++++++++++++ 13 files changed, 2026 insertions(+), 41 deletions(-) create mode 100644 spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SimplePropertySqlParameterSource.java create mode 100644 spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/DefaultJdbcClient.java create mode 100644 spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/JdbcClient.java create mode 100644 spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientIndexedParameterTests.java create mode 100644 spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientNamedParameterTests.java create mode 100644 spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientQueryTests.java diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcOperations.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcOperations.java index 75937f348bb..69aa01f6945 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcOperations.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,12 +35,19 @@ import org.springframework.lang.Nullable; *

Alternatively, the standard JDBC infrastructure can be mocked. * However, mocking this interface constitutes significantly less work. * As an alternative to a mock objects approach to testing data access code, - * consider the powerful integration testing support provided via the Spring - * TestContext Framework, in the {@code spring-test} artifact. + * consider the powerful integration testing support provided via the + * Spring TestContext Framework, in the {@code spring-test} artifact. + * + *

NOTE: As of 6.1, there is a unified JDBC access facade available in + * the form of {@link org.springframework.jdbc.core.simple.JdbcClient}. + * {@code JdbcClient} provides a fluent API style for common JDBC queries/updates + * with flexible use of indexed or named parameters. It delegates to + * {@code JdbcOperations}/{@code NamedParameterJdbcOperations} for actual execution. * * @author Rod Johnson * @author Juergen Hoeller * @see JdbcTemplate + * @see org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations */ public interface JdbcOperations { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java index 4ca00bd4b30..38c059330c0 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -65,8 +65,7 @@ import org.springframework.util.StringUtils; * It executes core JDBC workflow, leaving application code to provide SQL * and extract results. This class executes SQL queries or updates, initiating * iteration over ResultSets and catching JDBC exceptions and translating - * them to the generic, more informative exception hierarchy defined in the - * {@code org.springframework.dao} package. + * them to the common {@code org.springframework.dao} exception hierarchy. * *

Code using this class need only implement callback interfaces, giving * them a clearly defined contract. The {@link PreparedStatementCreator} callback @@ -75,7 +74,8 @@ import org.springframework.util.StringUtils; * values from a ResultSet. See also {@link PreparedStatementSetter} and * {@link RowMapper} for two popular alternative callback interfaces. * - *

Can be used within a service implementation via direct instantiation + *

An instance of this template class is thread-safe once configured. + * Can be used within a service implementation via direct instantiation * with a DataSource reference, or get prepared in an application context * and given to services as bean reference. Note: The DataSource should * always be configured as a bean in the application context, in the first case @@ -88,12 +88,17 @@ import org.springframework.util.StringUtils; *

All SQL operations performed by this class are logged at debug level, * using "org.springframework.jdbc.core.JdbcTemplate" as log category. * - *

NOTE: An instance of this class is thread-safe once configured. + *

NOTE: As of 6.1, there is a unified JDBC access facade available in + * the form of {@link org.springframework.jdbc.core.simple.JdbcClient}. + * {@code JdbcClient} provides a fluent API style for common JDBC queries/updates + * with flexible use of indexed or named parameters. It delegates to a + * {@code JdbcTemplate}/{@code NamedParameterJdbcTemplate} for actual execution. * * @author Rod Johnson * @author Juergen Hoeller * @author Thomas Risberg * @since May 3, 2001 + * @see JdbcOperations * @see PreparedStatementCreator * @see PreparedStatementSetter * @see CallableStatementCreator @@ -103,6 +108,7 @@ import org.springframework.util.StringUtils; * @see RowCallbackHandler * @see RowMapper * @see org.springframework.jdbc.support.SQLExceptionTranslator + * @see org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate */ public class JdbcTemplate extends JdbcAccessor implements JdbcOperations { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/BeanPropertySqlParameterSource.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/BeanPropertySqlParameterSource.java index 83b8edb24b9..bb2c595b653 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/BeanPropertySqlParameterSource.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/BeanPropertySqlParameterSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,15 +32,16 @@ import org.springframework.util.StringUtils; /** * {@link SqlParameterSource} implementation that obtains parameter values * from bean properties of a given JavaBean object. The names of the bean - * properties have to match the parameter names. + * properties have to match the parameter names. Supports components of + * record classes as well, with accessor methods matching parameter names. * - *

Uses a Spring BeanWrapper for bean property access underneath. + *

Uses a Spring {@link BeanWrapper} for bean property access underneath. * * @author Thomas Risberg * @author Juergen Hoeller * @since 2.0 * @see NamedParameterJdbcTemplate - * @see org.springframework.beans.BeanWrapper + * @see SimplePropertySqlParameterSource */ public class BeanPropertySqlParameterSource extends AbstractSqlParameterSource { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/MapSqlParameterSource.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/MapSqlParameterSource.java index 01518bff826..e3502ac63de 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/MapSqlParameterSource.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/MapSqlParameterSource.java @@ -143,6 +143,14 @@ public class MapSqlParameterSource extends AbstractSqlParameterSource { return this; } + /** + * Return whether this parameter source has been configured with any values. + * @since 6.1 + */ + public boolean hasValues() { + return !this.values.isEmpty(); + } + /** * Expose the current parameter values as read-only Map. */ diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcOperations.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcOperations.java index b308e06f735..95d99024d12 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcOperations.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,6 +40,12 @@ import org.springframework.lang.Nullable; * often used directly, but provides a useful option to enhance testability, * as it can easily be mocked or stubbed. * + *

NOTE: As of 6.1, there is a unified JDBC access facade available in + * the form of {@link org.springframework.jdbc.core.simple.JdbcClient}. + * {@code JdbcClient} provides a fluent API style for common JDBC queries/updates + * with flexible use of indexed or named parameters. It delegates to + * {@code JdbcOperations}/{@code NamedParameterJdbcOperations} for actual execution. + * * @author Thomas Risberg * @author Juergen Hoeller * @since 2.0 diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.java index 57e02e3595d..d2e91c208ed 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -55,16 +55,25 @@ import org.springframework.util.ConcurrentLruCache; * done at execution time. It also allows for expanding a {@link java.util.List} * of values to the appropriate number of placeholders. * - *

The underlying {@link org.springframework.jdbc.core.JdbcTemplate} is + *

An instance of this template class is thread-safe once configured. + * The underlying {@link org.springframework.jdbc.core.JdbcTemplate} is * exposed to allow for convenient access to the traditional * {@link org.springframework.jdbc.core.JdbcTemplate} methods. * - *

NOTE: An instance of this class is thread-safe once configured. + *

NOTE: As of 6.1, there is a unified JDBC access facade available in + * the form of {@link org.springframework.jdbc.core.simple.JdbcClient}. + * {@code JdbcClient} provides a fluent API style for common JDBC queries/updates + * with flexible use of indexed or named parameters. It delegates to a + * {@code JdbcTemplate}/{@code NamedParameterJdbcTemplate} for actual execution. * * @author Thomas Risberg * @author Juergen Hoeller * @since 2.0 * @see NamedParameterJdbcOperations + * @see SqlParameterSource + * @see ResultSetExtractor + * @see RowCallbackHandler + * @see RowMapper * @see org.springframework.jdbc.core.JdbcTemplate */ public class NamedParameterJdbcTemplate implements NamedParameterJdbcOperations { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SimplePropertySqlParameterSource.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SimplePropertySqlParameterSource.java new file mode 100644 index 00000000000..f5c873ef50d --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SimplePropertySqlParameterSource.java @@ -0,0 +1,117 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * 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 + * + * https://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 org.springframework.jdbc.core.namedparam; + +import java.beans.PropertyDescriptor; +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.beans.BeanUtils; +import org.springframework.jdbc.core.StatementCreatorUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; + +/** + * {@link SqlParameterSource} implementation that obtains parameter values + * from bean properties of a given JavaBean object, from component accessors + * of a record class, or from raw field access. + * + *

This is a more flexible variant of {@link BeanPropertySqlParameterSource}, + * with the limitation that it is not able to enumerate its + * {@link #getParameterNames() parameter names}. + * + *

In terms of its fallback property discovery algorithm, this class is + * similar to {@link org.springframework.validation.SimpleErrors} which is + * also just used for property retrieval purposes (rather than binding). + * + * @author Juergen Hoeller + * @since 6.1 + * @see NamedParameterJdbcTemplate + * @see BeanPropertySqlParameterSource + */ +public class SimplePropertySqlParameterSource extends AbstractSqlParameterSource { + + private final Object paramObject; + + private final Map descriptorMap = new HashMap<>(); + + + /** + * Create a new SqlParameterSource for the given bean, record or field holder. + * @param paramObject the bean, record or field holder instance to wrap + */ + public SimplePropertySqlParameterSource(Object paramObject) { + Assert.notNull(paramObject, "Parameter object must not be null"); + this.paramObject = paramObject; + } + + + @Override + public boolean hasValue(String paramName) { + return (getDescriptor(paramName) != null); + } + + @Override + @Nullable + public Object getValue(String paramName) throws IllegalArgumentException { + Object desc = getDescriptor(paramName); + if (desc instanceof PropertyDescriptor pd) { + ReflectionUtils.makeAccessible(pd.getReadMethod()); + return ReflectionUtils.invokeMethod(pd.getReadMethod(), this.paramObject); + } + else if (desc instanceof Field field) { + ReflectionUtils.makeAccessible(field); + return ReflectionUtils.getField(field, this.paramObject); + } + throw new IllegalArgumentException("Cannot retrieve value for parameter '" + paramName + + "' - neither a getter method nor a raw field found"); + } + + /** + * Derives a default SQL type from the corresponding property type. + * @see StatementCreatorUtils#javaTypeToSqlParameterType + */ + @Override + public int getSqlType(String paramName) { + int sqlType = super.getSqlType(paramName); + if (sqlType != TYPE_UNKNOWN) { + return sqlType; + } + Object desc = getDescriptor(paramName); + if (desc instanceof PropertyDescriptor pd) { + return StatementCreatorUtils.javaTypeToSqlParameterType(pd.getPropertyType()); + } + else if (desc instanceof Field field) { + return StatementCreatorUtils.javaTypeToSqlParameterType(field.getType()); + } + return TYPE_UNKNOWN; + } + + @Nullable + private Object getDescriptor(String paramName) { + return this.descriptorMap.computeIfAbsent(paramName, name -> { + Object pd = BeanUtils.getPropertyDescriptor(this.paramObject.getClass(), name); + if (pd == null) { + pd = ReflectionUtils.findField(this.paramObject.getClass(), name); + } + return pd; + }); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/DefaultJdbcClient.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/DefaultJdbcClient.java new file mode 100644 index 00000000000..61bad19c499 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/DefaultJdbcClient.java @@ -0,0 +1,313 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * 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 + * + * https://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 org.springframework.jdbc.core.simple; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import javax.sql.DataSource; + +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.ResultSetExtractor; +import org.springframework.jdbc.core.RowCallbackHandler; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.SqlParameterValue; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.namedparam.SimplePropertySqlParameterSource; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.jdbc.support.rowset.SqlRowSet; +import org.springframework.util.Assert; + +/** + * The default implementation of {@link JdbcClient}, + * as created by the static factory methods. + * + * @author Juergen Hoeller + * @since 6.1 + * @see JdbcClient#create(DataSource) + * @see JdbcClient#create(JdbcOperations) + * @see JdbcClient#create(NamedParameterJdbcOperations) + */ +class DefaultJdbcClient implements JdbcClient { + + private final JdbcOperations classicOps; + + private final NamedParameterJdbcOperations namedParamOps; + + + public DefaultJdbcClient(DataSource dataSource) { + this.classicOps = new JdbcTemplate(dataSource); + this.namedParamOps = new NamedParameterJdbcTemplate(this.classicOps); + } + + public DefaultJdbcClient(JdbcOperations jdbcTemplate) { + Assert.notNull(jdbcTemplate, "JdbcTemplate must not be null"); + this.classicOps = jdbcTemplate; + this.namedParamOps = new NamedParameterJdbcTemplate(jdbcTemplate); + } + + public DefaultJdbcClient(NamedParameterJdbcOperations jdbcTemplate) { + Assert.notNull(jdbcTemplate, "JdbcTemplate must not be null"); + this.classicOps = jdbcTemplate.getJdbcOperations(); + this.namedParamOps = jdbcTemplate; + } + + + @Override + public StatementSpec sql(String sql) { + return new DefaultStatementSpec(sql); + } + + + private class DefaultStatementSpec implements StatementSpec { + + private final String sql; + + private final List indexedParams = new ArrayList<>(); + + private final MapSqlParameterSource namedParams = new MapSqlParameterSource(); + + private SqlParameterSource namedParamSource = this.namedParams; + + public DefaultStatementSpec(String sql) { + this.sql = sql; + } + + @Override + public StatementSpec param(Object value) { + this.indexedParams.add(value); + return this; + } + + @Override + public StatementSpec param(int jdbcIndex, Object value) { + if (jdbcIndex < 1) { + throw new IllegalArgumentException("Invalid JDBC index: needs to start at 1"); + } + int index = jdbcIndex - 1; + int size = this.indexedParams.size(); + if (index < size) { + this.indexedParams.set(index, value); + } + else { + for (int i = size; i < index; i++) { + this.indexedParams.add(null); + } + this.indexedParams.add(value); + } + return this; + } + + @Override + public StatementSpec param(int jdbcIndex, Object value, int sqlType) { + return param(jdbcIndex, new SqlParameterValue(sqlType, value)); + } + + @Override + public StatementSpec param(String name, Object value) { + this.namedParams.addValue(name, value); + return this; + } + + @Override + public StatementSpec param(String name, Object value, int sqlType) { + this.namedParams.addValue(name, value, sqlType); + return this; + } + + @Override + public StatementSpec params(List values) { + this.indexedParams.addAll(values); + return this; + } + + @Override + public StatementSpec params(Map paramMap) { + this.namedParams.addValues(paramMap); + return this; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override + public StatementSpec paramSource(Object namedParamObject) { + this.namedParamSource = (namedParamObject instanceof Map map ? + new MapSqlParameterSource(map) : + new SimplePropertySqlParameterSource(namedParamObject)); + return this; + } + + @Override + public StatementSpec paramSource(SqlParameterSource namedParamSource) { + this.namedParamSource = namedParamSource; + return this; + } + + @Override + public ResultQuerySpec query() { + return (useNamedParams() ? + new NamedParamResultQuerySpec() : + new IndexedParamResultQuerySpec()); + } + + @Override + public MappedQuerySpec query(RowMapper rowMapper) { + return (useNamedParams() ? + new NamedParamMappedQuerySpec<>(rowMapper) : + new IndexedParamMappedQuerySpec<>(rowMapper)); + } + + @Override + public void query(RowCallbackHandler rch) { + if (useNamedParams()) { + namedParamOps.query(this.sql, this.namedParams, rch); + } + else { + classicOps.query(this.sql, rch, this.indexedParams.toArray()); + } + } + + @Override + public T query(ResultSetExtractor rse) { + T result = (useNamedParams() ? + namedParamOps.query(this.sql, this.namedParams, rse) : + classicOps.query(this.sql, rse, this.indexedParams.toArray())); + Assert.state(result != null, "No result from ResultSetExtractor"); + return result; + } + + @Override + public int update() { + return (useNamedParams() ? + namedParamOps.update(this.sql, this.namedParamSource) : + classicOps.update(this.sql, this.indexedParams.toArray())); + } + + @Override + public int update(KeyHolder generatedKeyHolder) { + return (useNamedParams() ? + namedParamOps.update(this.sql, this.namedParamSource, generatedKeyHolder) : + classicOps.update(this.sql, this.indexedParams.toArray(), generatedKeyHolder)); + } + + private boolean useNamedParams() { + boolean hasNamedParams = (this.namedParams.hasValues() || this.namedParamSource != this.namedParams); + if (hasNamedParams && !this.indexedParams.isEmpty()) { + throw new IllegalStateException("Configure either named or indexed parameters, not both"); + } + if (this.namedParams.hasValues() && this.namedParamSource != this.namedParams) { + throw new IllegalStateException( + "Configure either individual named parameters or a SqlParameterSource, not both"); + } + return hasNamedParams; + } + + + private class IndexedParamResultQuerySpec implements ResultQuerySpec { + + @Override + public SqlRowSet rowSet() { + return classicOps.queryForRowSet(sql, indexedParams.toArray()); + } + + @Override + public List> listOfRows() { + return classicOps.queryForList(sql, indexedParams.toArray()); + } + + @Override + public Map singleRow() { + return classicOps.queryForMap(sql, indexedParams.toArray()); + } + + @Override + public List singleColumn(Class requiredType) { + return classicOps.queryForList(sql, requiredType, indexedParams.toArray()); + } + } + + + private class NamedParamResultQuerySpec implements ResultQuerySpec { + + @Override + public SqlRowSet rowSet() { + return namedParamOps.queryForRowSet(sql, namedParamSource); + } + + @Override + public List> listOfRows() { + return namedParamOps.queryForList(sql, namedParamSource); + } + + @Override + public Map singleRow() { + return namedParamOps.queryForMap(sql, namedParamSource); + } + + @Override + public List singleColumn(Class requiredType) { + return namedParamOps.queryForList(sql, namedParamSource, requiredType); + } + } + + + private class IndexedParamMappedQuerySpec implements MappedQuerySpec { + + private final RowMapper rowMapper; + + public IndexedParamMappedQuerySpec(RowMapper rowMapper) { + this.rowMapper = rowMapper; + } + + @Override + public Stream stream() { + return classicOps.queryForStream(sql, this.rowMapper, indexedParams.toArray()); + } + + @Override + public List list() { + return classicOps.query(sql, this.rowMapper, indexedParams.toArray()); + } + } + + + private class NamedParamMappedQuerySpec implements MappedQuerySpec { + + private final RowMapper rowMapper; + + public NamedParamMappedQuerySpec(RowMapper rowMapper) { + this.rowMapper = rowMapper; + } + + @Override + public Stream stream() { + return namedParamOps.queryForStream(sql, namedParamSource, this.rowMapper); + } + + @Override + public List list() { + return namedParamOps.query(sql, namedParamSource, this.rowMapper); + } + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/JdbcClient.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/JdbcClient.java new file mode 100644 index 00000000000..5b529b349f5 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/JdbcClient.java @@ -0,0 +1,366 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * 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 + * + * https://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 org.springframework.jdbc.core.simple; + +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +import javax.sql.DataSource; + +import org.springframework.dao.support.DataAccessUtils; +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.jdbc.core.ResultSetExtractor; +import org.springframework.jdbc.core.RowCallbackHandler; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.jdbc.support.rowset.SqlRowSet; + +/** + * A fluent {@link JdbcClient} with common JDBC query and update operations, + * supporting JDBC-style positional as well as Spring-style named parameters + * with a convenient unified facade for JDBC PreparedStatement execution. + * + *

An example for retrieving a query result as a {@code java.util.Optional}: + *

+ * Optional<Integer> value = client.sql("SELECT AGE FROM CUSTMR WHERE ID = :id")
+ *     .param("id", 3)
+ *     .query((rs, rowNum) -> rs.getInt(1))
+ *     .optional();
+ * 
+ * + *

Delegates to {@link org.springframework.jdbc.core.JdbcTemplate} and + * {@link org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate}. + * For complex JDBC operations, e.g. batch inserts and stored procedure calls, + * you may use those lower-level template classes directly - or alternatively, + * {@link SimpleJdbcInsert} and {@link SimpleJdbcCall}. + * + * @author Juergen Hoeller + * @since 6.1 + * @see ResultSetExtractor + * @see RowCallbackHandler + * @see RowMapper + * @see JdbcOperations + * @see NamedParameterJdbcOperations + * @see org.springframework.jdbc.core.JdbcTemplate + * @see org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate + */ +public interface JdbcClient { + + /** + * The starting point for any JDBC operation: a custom SQL String. + * @param sql the SQL query or update statement as a String + * @return a chained statement specification + */ + StatementSpec sql(String sql); + + + // Static factory methods + + /** + * Create a {@code JdbcClient} for the given {@link DataSource}. + * @param dataSource the DataSource to obtain connections from + */ + static JdbcClient create(DataSource dataSource) { + return new DefaultJdbcClient(dataSource); + } + + /** + * Create a {@code JdbcClient} for the given {@link JdbcOperations} delegate, + * typically a {@link org.springframework.jdbc.core.JdbcTemplate}. + *

Use this factory method for reusing existing {@code JdbcTemplate} configuration, + * including its {@code DataSource}. + * @param jdbcTemplate the delegate to perform operations on + */ + static JdbcClient create(JdbcOperations jdbcTemplate) { + return new DefaultJdbcClient(jdbcTemplate); + } + + /** + * Create a {@code JdbcClient} for the given {@link NamedParameterJdbcOperations} delegate, + * typically a {@link org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate}. + *

Use this factory method for reusing existing {@code NamedParameterJdbcTemplate} + * configuration, including its underlying {@code JdbcTemplate} and the {@code DataSource}. + * @param jdbcTemplate the delegate to perform operations on + */ + static JdbcClient create(NamedParameterJdbcOperations jdbcTemplate) { + return new DefaultJdbcClient(jdbcTemplate); + } + + + /** + * A statement specification for parameter bindings and query/update execution. + */ + interface StatementSpec { + + /** + * Bind a positional JDBC statement parameter for "?" placeholder resolution + * by implicit order of parameter value registration. + *

This is primarily intended for statements with a single or very few + * parameters, registering each parameter value in the order of the + * parameter's occurrence in the SQL statement. + * @param value the parameter value to bind + * @return this statement specification (for chaining) + * @see java.sql.PreparedStatement#setObject(int, Object) + */ + StatementSpec param(Object value); + + /** + * Bind a positional JDBC statement parameter for "?" placeholder resolution + * by explicit JDBC statement parameter index. + * @param jdbcIndex the JDBC-style index (starting with 1) + * @param value the parameter value to bind + * @return this statement specification (for chaining) + * @see java.sql.PreparedStatement#setObject(int, Object) + */ + StatementSpec param(int jdbcIndex, Object value); + + /** + * Bind a positional JDBC statement parameter for "?" placeholder resolution + * by explicit JDBC statement parameter index. + * @param jdbcIndex the JDBC-style index (starting with 1) + * @param value the parameter value to bind + * @param sqlType the associated SQL type (see {@link java.sql.Types}) + * @return this statement specification (for chaining) + * @see java.sql.PreparedStatement#setObject(int, Object, int) + */ + StatementSpec param(int jdbcIndex, Object value, int sqlType); + + /** + * Bind a named statement parameter for ":x" placeholder resolution, + * with each "x" name matching a ":x" placeholder in the SQL statement. + * @param name the parameter name + * @param value the parameter value to bind + * @return this statement specification (for chaining) + * @see org.springframework.jdbc.core.namedparam.MapSqlParameterSource#addValue(String, Object) + */ + StatementSpec param(String name, Object value); + + /** + * Bind a named statement parameter for ":x" placeholder resolution, + * with each "x" name matching a ":x" placeholder in the SQL statement. + * @param name the parameter name + * @param value the parameter value to bind + * @param sqlType the associated SQL type (see {@link java.sql.Types}) + * @return this statement specification (for chaining) + * @see org.springframework.jdbc.core.namedparam.MapSqlParameterSource#addValue(String, Object, int) + */ + StatementSpec param(String name, Object value, int sqlType); + + /** + * Bind a list of positional parameters for "?" placeholder resolution. + *

The given list will be added to existing positional parameters, if any. + * Each element from the complete list will be bound as a JDBC positional + * parameter with a corresponding JDBC index (i.e. list index + 1). + * @param values the parameter values to bind + * @return this statement specification (for chaining) + * @see #param(Object) + */ + StatementSpec params(List values); + + /** + * Bind named statement parameters for ":x" placeholder resolution. + *

The given map will be merged into existing named parameters, if any. + * @param paramMap a map of names and parameter values to bind + * @return this statement specification (for chaining) + * @see #param(String, Object) + */ + StatementSpec params(Map paramMap); + + /** + * Bind named statement parameters for ":x" placeholder resolution. + *

The given parameter object will define all named parameters + * based on its JavaBean properties, record components or raw fields. + * A Map instance can be provided as a complete parameter source as well. + * @param namedParamObject a custom parameter object (e.g. a JavaBean or + * record class) with named properties serving as statement parameters + * @return this statement specification (for chaining) + * @see org.springframework.jdbc.core.namedparam.MapSqlParameterSource + * @see org.springframework.jdbc.core.namedparam.SimplePropertySqlParameterSource + */ + StatementSpec paramSource(Object namedParamObject); + + /** + * Bind named statement parameters for ":x" placeholder resolution. + *

The given parameter source will define all named parameters, + * possibly associating specific SQL types with each value. + * @param namedParamSource a custom {@link SqlParameterSource} instance + * @return this statement specification (for chaining) + * @see org.springframework.jdbc.core.namedparam.AbstractSqlParameterSource#registerSqlType + */ + StatementSpec paramSource(SqlParameterSource namedParamSource); + + /** + * Proceed towards execution of a query, with several result options + * available in the returned query specification. + * @return the result query specification + * @see java.sql.PreparedStatement#executeQuery() + */ + ResultQuerySpec query(); + + /** + * Proceed towards execution of a mapped query, with several options + * available in the returned query specification. + * @param rowMapper the callback for mapping each row in the ResultSet + * @return the mapped query specification + * @see java.sql.PreparedStatement#executeQuery() + */ + MappedQuerySpec query(RowMapper rowMapper); + + /** + * Execute a query with the provided SQL statement, + * processing each row with the given callback. + * @param rch a callback for processing each row in the ResultSet + * @see java.sql.PreparedStatement#executeQuery() + */ + void query(RowCallbackHandler rch); + + /** + * Execute a query with the provided SQL statement, + * returning a result object for the entire ResultSet. + * @param rse a callback for processing the entire ResultSet + * @return the value returned by the ResultSetExtractor + * @see java.sql.PreparedStatement#executeQuery() + */ + T query(ResultSetExtractor rse); + + /** + * Execute the provided SQL statement as an update. + * @return the number of rows affected + * @see java.sql.PreparedStatement#executeUpdate() + */ + int update(); + + /** + * Execute the provided SQL statement as an update. + * @param generatedKeyHolder a KeyHolder that will hold the generated keys + * (typically a {@link org.springframework.jdbc.support.GeneratedKeyHolder}) + * @return the number of rows affected + * @see java.sql.PreparedStatement#executeUpdate() + */ + int update(KeyHolder generatedKeyHolder); + } + + + /** + * A specification for simple result queries. + */ + interface ResultQuerySpec { + + /** + * Retrieve the result as a row set. + * @return a detached row set representation + * of the original database result + */ + SqlRowSet rowSet(); + + /** + * Retrieve the result as a list of rows, + * retaining the order from the original database result. + * @return a (potentially empty) list of rows, + * with each result row represented as a map of + * case-insensitive column names to column values + */ + List> listOfRows(); + + /** + * Retrieve a single row result. + * @return the result row represented as a map of + * case-insensitive column names to column values + */ + Map singleRow(); + + /** + * Retrieve a single column result, + * retaining the order from the original database result. + * @return a (potentially empty) list of rows, with each + * row represented as a column value of the given type + */ + List singleColumn(Class requiredType); + + /** + * Retrieve a single value result. + * @return the single row represented as its single + * column value of the given type + * @see DataAccessUtils#requiredSingleResult(Collection) + */ + default T singleValue(Class requiredType) { + return DataAccessUtils.requiredSingleResult(singleColumn(requiredType)); + } + } + + + /** + * A specification for RowMapper-mapped queries. + * + * @param the RowMapper-declared result type + */ + interface MappedQuerySpec { + + /** + * Retrieve the result as a lazily resolved stream of mapped objects, + * retaining the order from the original database result. + * @return the result Stream, containing mapped objects, needing to be + * closed once fully processed (e.g. through a try-with-resources clause) + */ + Stream stream(); + + /** + * Retrieve the result as a pre-resolved list of mapped objects, + * retaining the order from the original database result. + * @return the result as a detached List, containing mapped objects + */ + List list(); + + /** + * Retrieve the result as an order-preserving set of mapped objects. + * @return the result as a detached Set, containing mapped objects + * @see #list() + * @see LinkedHashSet + */ + default Set set() { + return new LinkedHashSet<>(list()); + } + + /** + * Retrieve a single result, if available, as an {@link Optional} handle. + * @return an Optional handle with a single result object or none + * @see #list() + * @see DataAccessUtils#optionalResult(Collection) + */ + default Optional optional() { + return DataAccessUtils.optionalResult(list()); + } + + /** + * Retrieve a single result as a required object instance. + * @return the single result object (never {@code null}) + * @see #list() + * @see DataAccessUtils#requiredSingleResult(Collection) + */ + default T single() { + return DataAccessUtils.requiredSingleResult(list()); + } + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterQueryTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterQueryTests.java index b7c438cbb11..48073844c1b 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterQueryTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterQueryTests.java @@ -34,8 +34,6 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.jdbc.core.RowMapper; - import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; @@ -129,8 +127,7 @@ public class NamedParameterQueryTests { } @Test - public void testQueryForListWithParamMapAndIntegerElementAndSingleRowAndColumn() - throws Exception { + public void testQueryForListWithParamMapAndIntegerElementAndSingleRowAndColumn() throws Exception { given(resultSet.getMetaData()).willReturn(resultSetMetaData); given(resultSet.next()).willReturn(true, false); given(resultSet.getInt(1)).willReturn(11); @@ -168,11 +165,10 @@ public class NamedParameterQueryTests { MapSqlParameterSource params = new MapSqlParameterSource(); params.addValue("id", 3); - Object o = template.queryForObject("SELECT AGE FROM CUSTMR WHERE ID = :id", - params, (RowMapper) (rs, rowNum) -> rs.getInt(1)); + Integer value = template.queryForObject("SELECT AGE FROM CUSTMR WHERE ID = :id", + params, (rs, rowNum) -> rs.getInt(1)); - boolean condition = o instanceof Integer; - assertThat(condition).as("Correct result type").isTrue(); + assertThat(value).isEqualTo(22); verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID = ?"); verify(preparedStatement).setObject(1, 3); } @@ -185,11 +181,10 @@ public class NamedParameterQueryTests { Map params = new HashMap<>(); params.put("id", 3); - Object o = template.queryForObject("SELECT AGE FROM CUSTMR WHERE ID = :id", + Integer value = template.queryForObject("SELECT AGE FROM CUSTMR WHERE ID = :id", params, Integer.class); - boolean condition = o instanceof Integer; - assertThat(condition).as("Correct result type").isTrue(); + assertThat(value).isEqualTo(22); verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID = ?"); verify(preparedStatement).setObject(1, 3); } @@ -202,30 +197,26 @@ public class NamedParameterQueryTests { MapSqlParameterSource params = new MapSqlParameterSource(); params.addValue("id", 3); - Object o = template.queryForObject("SELECT AGE FROM CUSTMR WHERE ID = :id", + Integer value = template.queryForObject("SELECT AGE FROM CUSTMR WHERE ID = :id", params, Integer.class); - boolean condition = o instanceof Integer; - assertThat(condition).as("Correct result type").isTrue(); + assertThat(value).isEqualTo(22); verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID = ?"); verify(preparedStatement).setObject(1, 3); } @Test public void testQueryForObjectWithParamMapAndList() throws Exception { - String sql = "SELECT AGE FROM CUSTMR WHERE ID IN (:ids)"; - String sqlToUse = "SELECT AGE FROM CUSTMR WHERE ID IN (?, ?)"; given(resultSet.getMetaData()).willReturn(resultSetMetaData); given(resultSet.next()).willReturn(true, false); given(resultSet.getInt(1)).willReturn(22); MapSqlParameterSource params = new MapSqlParameterSource(); params.addValue("ids", Arrays.asList(3, 4)); - Object o = template.queryForObject(sql, params, Integer.class); + Integer value = template.queryForObject("SELECT AGE FROM CUSTMR WHERE ID IN (:ids)", params, Integer.class); - boolean condition = o instanceof Integer; - assertThat(condition).as("Correct result type").isTrue(); - verify(connection).prepareStatement(sqlToUse); + assertThat(value).isEqualTo(22); + verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID IN (?, ?)"); verify(preparedStatement).setObject(1, 3); } @@ -240,14 +231,11 @@ public class NamedParameterQueryTests { l1.add(new Object[] {3, "Rod"}); l1.add(new Object[] {4, "Juergen"}); params.addValue("multiExpressionList", l1); - Object o = template.queryForObject( - "SELECT AGE FROM CUSTMR WHERE (ID, NAME) IN (:multiExpressionList)", + Integer value = template.queryForObject("SELECT AGE FROM CUSTMR WHERE (ID, NAME) IN (:multiExpressionList)", params, Integer.class); - boolean condition = o instanceof Integer; - assertThat(condition).as("Correct result type").isTrue(); - verify(connection).prepareStatement( - "SELECT AGE FROM CUSTMR WHERE (ID, NAME) IN ((?, ?), (?, ?))"); + assertThat(value).isEqualTo(22); + verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE (ID, NAME) IN ((?, ?), (?, ?))"); verify(preparedStatement).setObject(1, 3); } @@ -295,6 +283,20 @@ public class NamedParameterQueryTests { verify(preparedStatement).setObject(2, 5); } + @Test + public void testQueryForLongWithParamRecord() throws Exception { + given(resultSet.getMetaData()).willReturn(resultSetMetaData); + given(resultSet.next()).willReturn(true, false); + given(resultSet.getLong(1)).willReturn(87L); + + BeanPropertySqlParameterSource params = new BeanPropertySqlParameterSource(new ParameterRecord(3)); + long l = template.queryForObject("SELECT AGE FROM CUSTMR WHERE ID = :id", params, Long.class); + + assertThat(l).as("Return of a long").isEqualTo(87); + verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID = ?"); + verify(preparedStatement).setObject(1, 3, Types.INTEGER); + } + static class ParameterBean { @@ -323,4 +325,8 @@ public class NamedParameterQueryTests { } } + + record ParameterRecord(int id) { + } + } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientIndexedParameterTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientIndexedParameterTests.java new file mode 100644 index 00000000000..1a07a2c76c9 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientIndexedParameterTests.java @@ -0,0 +1,332 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * 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 + * + * https://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 org.springframework.jdbc.core.simple; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.jdbc.Customer; +import org.springframework.jdbc.core.SqlParameterValue; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * @author Juergen Hoeller + * @since 6.1 + */ +public class JdbcClientIndexedParameterTests { + + private static final String SELECT_NAMED_PARAMETERS = + "select id, forename from custmr where id = ? and country = ?"; + private static final String SELECT_NO_PARAMETERS = + "select id, forename from custmr"; + + private static final String UPDATE_NAMED_PARAMETERS = + "update seat_status set booking_id = null where performance_id = ? and price_band_id = ?"; + + private static final String[] COLUMN_NAMES = new String[] {"id", "forename"}; + + + private Connection connection = mock(); + + private DataSource dataSource = mock(); + + private PreparedStatement preparedStatement = mock(); + + private ResultSet resultSet = mock(); + + private DatabaseMetaData databaseMetaData = mock(); + + private JdbcClient client = JdbcClient.create(dataSource); + + private List params = new ArrayList<>(); + + + @BeforeEach + public void setup() throws Exception { + given(dataSource.getConnection()).willReturn(connection); + given(connection.prepareStatement(anyString())).willReturn(preparedStatement); + given(preparedStatement.getConnection()).willReturn(connection); + given(preparedStatement.executeQuery()).willReturn(resultSet); + given(databaseMetaData.getDatabaseProductName()).willReturn("MySQL"); + given(databaseMetaData.supportsBatchUpdates()).willReturn(true); + } + + + @Test + public void testQueryWithResultSetExtractor() throws SQLException { + given(resultSet.next()).willReturn(true); + given(resultSet.getInt("id")).willReturn(1); + given(resultSet.getString("forename")).willReturn("rod"); + + params.add(new SqlParameterValue(Types.DECIMAL, 1)); + params.add("UK"); + Customer cust = client.sql(SELECT_NAMED_PARAMETERS).params(params).query( + rs -> { + rs.next(); + Customer cust1 = new Customer(); + cust1.setId(rs.getInt(COLUMN_NAMES[0])); + cust1.setForename(rs.getString(COLUMN_NAMES[1])); + return cust1; + }); + + assertThat(cust.getId()).as("Customer id was assigned correctly").isEqualTo(1); + assertThat(cust.getForename()).as("Customer forename was assigned correctly").isEqualTo("rod"); + verify(connection).prepareStatement(SELECT_NAMED_PARAMETERS); + verify(preparedStatement).setObject(1, 1, Types.DECIMAL); + verify(preparedStatement).setString(2, "UK"); + verify(resultSet).close(); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + public void testQueryWithResultSetExtractorNoParameters() throws SQLException { + given(resultSet.next()).willReturn(true); + given(resultSet.getInt("id")).willReturn(1); + given(resultSet.getString("forename")).willReturn("rod"); + + Customer cust = client.sql(SELECT_NO_PARAMETERS).query( + rs -> { + rs.next(); + Customer cust1 = new Customer(); + cust1.setId(rs.getInt(COLUMN_NAMES[0])); + cust1.setForename(rs.getString(COLUMN_NAMES[1])); + return cust1; + }); + + assertThat(cust.getId()).as("Customer id was assigned correctly").isEqualTo(1); + assertThat(cust.getForename()).as("Customer forename was assigned correctly").isEqualTo("rod"); + verify(connection).prepareStatement(SELECT_NO_PARAMETERS); + verify(resultSet).close(); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + public void testQueryWithRowCallbackHandler() throws SQLException { + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt("id")).willReturn(1); + given(resultSet.getString("forename")).willReturn("rod"); + + params.add(new SqlParameterValue(Types.DECIMAL, 1)); + params.add("UK"); + final List customers = new ArrayList<>(); + client.sql(SELECT_NAMED_PARAMETERS).params(params).query(rs -> { + Customer cust = new Customer(); + cust.setId(rs.getInt(COLUMN_NAMES[0])); + cust.setForename(rs.getString(COLUMN_NAMES[1])); + customers.add(cust); + }); + + assertThat(customers).hasSize(1); + assertThat(customers.get(0).getId()).as("Customer id was assigned correctly").isEqualTo(1); + assertThat(customers.get(0).getForename()).as("Customer forename was assigned correctly").isEqualTo("rod"); + verify(connection).prepareStatement(SELECT_NAMED_PARAMETERS); + verify(preparedStatement).setObject(1, 1, Types.DECIMAL); + verify(preparedStatement).setString(2, "UK"); + verify(resultSet).close(); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + public void testQueryWithRowCallbackHandlerNoParameters() throws SQLException { + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt("id")).willReturn(1); + given(resultSet.getString("forename")).willReturn("rod"); + + final List customers = new ArrayList<>(); + client.sql(SELECT_NO_PARAMETERS).query(rs -> { + Customer cust = new Customer(); + cust.setId(rs.getInt(COLUMN_NAMES[0])); + cust.setForename(rs.getString(COLUMN_NAMES[1])); + customers.add(cust); + }); + + assertThat(customers).hasSize(1); + assertThat(customers.get(0).getId()).as("Customer id was assigned correctly").isEqualTo(1); + assertThat(customers.get(0).getForename()).as("Customer forename was assigned correctly").isEqualTo("rod"); + verify(connection).prepareStatement(SELECT_NO_PARAMETERS); + verify(resultSet).close(); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + public void testQueryWithRowMapper() throws SQLException { + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt("id")).willReturn(1); + given(resultSet.getString("forename")).willReturn("rod"); + + params.add(new SqlParameterValue(Types.DECIMAL, 1)); + params.add("UK"); + List customers = client.sql(SELECT_NAMED_PARAMETERS).params(params).query( + (rs, rownum) -> { + Customer cust = new Customer(); + cust.setId(rs.getInt(COLUMN_NAMES[0])); + cust.setForename(rs.getString(COLUMN_NAMES[1])); + return cust; + }).list(); + + assertThat(customers).hasSize(1); + Customer cust = customers.get(0); + assertThat(cust.getId()).as("Customer id was assigned correctly").isEqualTo(1); + assertThat(cust.getForename()).as("Customer forename was assigned correctly").isEqualTo("rod"); + verify(connection).prepareStatement(SELECT_NAMED_PARAMETERS); + verify(preparedStatement).setObject(1, 1, Types.DECIMAL); + verify(preparedStatement).setString(2, "UK"); + verify(resultSet).close(); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + public void testQueryWithRowMapperNoParameters() throws SQLException { + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt("id")).willReturn(1); + given(resultSet.getString("forename")).willReturn("rod"); + + Set customers = client.sql(SELECT_NO_PARAMETERS).query( + (rs, rownum) -> { + Customer cust = new Customer(); + cust.setId(rs.getInt(COLUMN_NAMES[0])); + cust.setForename(rs.getString(COLUMN_NAMES[1])); + return cust; + }).set(); + + assertThat(customers).hasSize(1); + Customer cust = customers.iterator().next(); + assertThat(cust.getId()).as("Customer id was assigned correctly").isEqualTo(1); + assertThat(cust.getForename()).as("Customer forename was assigned correctly").isEqualTo("rod"); + verify(connection).prepareStatement(SELECT_NO_PARAMETERS); + verify(resultSet).close(); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + public void testQueryForObjectWithRowMapper() throws SQLException { + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt("id")).willReturn(1); + given(resultSet.getString("forename")).willReturn("rod"); + + params.add(new SqlParameterValue(Types.DECIMAL, 1)); + params.add("UK"); + + Customer cust = client.sql(SELECT_NAMED_PARAMETERS).params(params).query( + (rs, rownum) -> { + Customer cust1 = new Customer(); + cust1.setId(rs.getInt(COLUMN_NAMES[0])); + cust1.setForename(rs.getString(COLUMN_NAMES[1])); + return cust1; + }).single(); + + assertThat(cust.getId()).as("Customer id was assigned correctly").isEqualTo(1); + assertThat(cust.getForename()).as("Customer forename was assigned correctly").isEqualTo("rod"); + verify(connection).prepareStatement(SELECT_NAMED_PARAMETERS); + verify(preparedStatement).setObject(1, 1, Types.DECIMAL); + verify(preparedStatement).setString(2, "UK"); + verify(resultSet).close(); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + public void testQueryForStreamWithRowMapper() throws SQLException { + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt("id")).willReturn(1); + given(resultSet.getString("forename")).willReturn("rod"); + + params.add(new SqlParameterValue(Types.DECIMAL, 1)); + params.add("UK"); + AtomicInteger count = new AtomicInteger(); + + try (Stream s = client.sql(SELECT_NAMED_PARAMETERS).params(params).query( + (rs, rownum) -> { + Customer cust1 = new Customer(); + cust1.setId(rs.getInt(COLUMN_NAMES[0])); + cust1.setForename(rs.getString(COLUMN_NAMES[1])); + return cust1; + }).stream()) { + s.forEach(cust -> { + count.incrementAndGet(); + assertThat(cust.getId()).as("Customer id was assigned correctly").isEqualTo(1); + assertThat(cust.getForename()).as("Customer forename was assigned correctly").isEqualTo("rod"); + }); + } + + assertThat(count.get()).isEqualTo(1); + verify(connection).prepareStatement(SELECT_NAMED_PARAMETERS); + verify(preparedStatement).setObject(1, 1, Types.DECIMAL); + verify(preparedStatement).setString(2, "UK"); + verify(resultSet).close(); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + public void testUpdate() throws SQLException { + given(preparedStatement.executeUpdate()).willReturn(1); + + params.add(1); + params.add(1); + int rowsAffected = client.sql(UPDATE_NAMED_PARAMETERS).params(params).update(); + + assertThat(rowsAffected).isEqualTo(1); + verify(connection).prepareStatement(UPDATE_NAMED_PARAMETERS); + verify(preparedStatement).setObject(1, 1); + verify(preparedStatement).setObject(2, 1); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + public void testUpdateWithTypedParameters() throws SQLException { + given(preparedStatement.executeUpdate()).willReturn(1); + + params.add(new SqlParameterValue(Types.DECIMAL, 1)); + params.add(new SqlParameterValue(Types.INTEGER, 1)); + int rowsAffected = client.sql(UPDATE_NAMED_PARAMETERS).params(params).update(); + + assertThat(rowsAffected).isEqualTo(1); + verify(connection).prepareStatement(UPDATE_NAMED_PARAMETERS); + verify(preparedStatement).setObject(1, 1, Types.DECIMAL); + verify(preparedStatement).setObject(2, 1, Types.INTEGER); + verify(preparedStatement).close(); + verify(connection).close(); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientNamedParameterTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientNamedParameterTests.java new file mode 100644 index 00000000000..cd3348b09ce --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientNamedParameterTests.java @@ -0,0 +1,338 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * 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 + * + * https://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 org.springframework.jdbc.core.simple; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.jdbc.Customer; +import org.springframework.jdbc.core.SqlParameterValue; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * @author Juergen Hoeller + * @since 6.1 + */ +public class JdbcClientNamedParameterTests { + + private static final String SELECT_NAMED_PARAMETERS = + "select id, forename from custmr where id = :id and country = :country"; + private static final String SELECT_NAMED_PARAMETERS_PARSED = + "select id, forename from custmr where id = ? and country = ?"; + private static final String SELECT_NO_PARAMETERS = + "select id, forename from custmr"; + + private static final String UPDATE_NAMED_PARAMETERS = + "update seat_status set booking_id = null where performance_id = :perfId and price_band_id = :priceId"; + private static final String UPDATE_NAMED_PARAMETERS_PARSED = + "update seat_status set booking_id = null where performance_id = ? and price_band_id = ?"; + + private static final String[] COLUMN_NAMES = new String[] {"id", "forename"}; + + + private Connection connection = mock(); + + private DataSource dataSource = mock(); + + private PreparedStatement preparedStatement = mock(); + + private ResultSet resultSet = mock(); + + private DatabaseMetaData databaseMetaData = mock(); + + private JdbcClient client = JdbcClient.create(dataSource); + + private Map params = new HashMap<>(); + + + @BeforeEach + public void setup() throws Exception { + given(dataSource.getConnection()).willReturn(connection); + given(connection.prepareStatement(anyString())).willReturn(preparedStatement); + given(preparedStatement.getConnection()).willReturn(connection); + given(preparedStatement.executeQuery()).willReturn(resultSet); + given(databaseMetaData.getDatabaseProductName()).willReturn("MySQL"); + given(databaseMetaData.supportsBatchUpdates()).willReturn(true); + } + + + @Test + public void testQueryWithResultSetExtractor() throws SQLException { + given(resultSet.next()).willReturn(true); + given(resultSet.getInt("id")).willReturn(1); + given(resultSet.getString("forename")).willReturn("rod"); + + params.put("id", new SqlParameterValue(Types.DECIMAL, 1)); + params.put("country", "UK"); + Customer cust = client.sql(SELECT_NAMED_PARAMETERS).params(params).query( + rs -> { + rs.next(); + Customer cust1 = new Customer(); + cust1.setId(rs.getInt(COLUMN_NAMES[0])); + cust1.setForename(rs.getString(COLUMN_NAMES[1])); + return cust1; + }); + + assertThat(cust.getId()).as("Customer id was assigned correctly").isEqualTo(1); + assertThat(cust.getForename()).as("Customer forename was assigned correctly").isEqualTo("rod"); + verify(connection).prepareStatement(SELECT_NAMED_PARAMETERS_PARSED); + verify(preparedStatement).setObject(1, 1, Types.DECIMAL); + verify(preparedStatement).setString(2, "UK"); + verify(resultSet).close(); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + public void testQueryWithResultSetExtractorNoParameters() throws SQLException { + given(resultSet.next()).willReturn(true); + given(resultSet.getInt("id")).willReturn(1); + given(resultSet.getString("forename")).willReturn("rod"); + + Customer cust = client.sql(SELECT_NO_PARAMETERS).query( + rs -> { + rs.next(); + Customer cust1 = new Customer(); + cust1.setId(rs.getInt(COLUMN_NAMES[0])); + cust1.setForename(rs.getString(COLUMN_NAMES[1])); + return cust1; + }); + + assertThat(cust.getId()).as("Customer id was assigned correctly").isEqualTo(1); + assertThat(cust.getForename()).as("Customer forename was assigned correctly").isEqualTo("rod"); + verify(connection).prepareStatement(SELECT_NO_PARAMETERS); + verify(resultSet).close(); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + public void testQueryWithRowCallbackHandler() throws SQLException { + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt("id")).willReturn(1); + given(resultSet.getString("forename")).willReturn("rod"); + + params.put("id", new SqlParameterValue(Types.DECIMAL, 1)); + params.put("country", "UK"); + final List customers = new ArrayList<>(); + client.sql(SELECT_NAMED_PARAMETERS).params(params).query(rs -> { + Customer cust = new Customer(); + cust.setId(rs.getInt(COLUMN_NAMES[0])); + cust.setForename(rs.getString(COLUMN_NAMES[1])); + customers.add(cust); + }); + + assertThat(customers).hasSize(1); + assertThat(customers.get(0).getId()).as("Customer id was assigned correctly").isEqualTo(1); + assertThat(customers.get(0).getForename()).as("Customer forename was assigned correctly").isEqualTo("rod"); + verify(connection).prepareStatement(SELECT_NAMED_PARAMETERS_PARSED); + verify(preparedStatement).setObject(1, 1, Types.DECIMAL); + verify(preparedStatement).setString(2, "UK"); + verify(resultSet).close(); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + public void testQueryWithRowCallbackHandlerNoParameters() throws SQLException { + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt("id")).willReturn(1); + given(resultSet.getString("forename")).willReturn("rod"); + + final List customers = new ArrayList<>(); + client.sql(SELECT_NO_PARAMETERS).query(rs -> { + Customer cust = new Customer(); + cust.setId(rs.getInt(COLUMN_NAMES[0])); + cust.setForename(rs.getString(COLUMN_NAMES[1])); + customers.add(cust); + }); + + assertThat(customers).hasSize(1); + assertThat(customers.get(0).getId()).as("Customer id was assigned correctly").isEqualTo(1); + assertThat(customers.get(0).getForename()).as("Customer forename was assigned correctly").isEqualTo("rod"); + verify(connection).prepareStatement(SELECT_NO_PARAMETERS); + verify(resultSet).close(); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + public void testQueryWithRowMapper() throws SQLException { + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt("id")).willReturn(1); + given(resultSet.getString("forename")).willReturn("rod"); + + params.put("id", new SqlParameterValue(Types.DECIMAL, 1)); + params.put("country", "UK"); + List customers = client.sql(SELECT_NAMED_PARAMETERS).params(params).query( + (rs, rownum) -> { + Customer cust = new Customer(); + cust.setId(rs.getInt(COLUMN_NAMES[0])); + cust.setForename(rs.getString(COLUMN_NAMES[1])); + return cust; + }).list(); + + assertThat(customers).hasSize(1); + Customer cust = customers.get(0); + assertThat(cust.getId()).as("Customer id was assigned correctly").isEqualTo(1); + assertThat(cust.getForename()).as("Customer forename was assigned correctly").isEqualTo("rod"); + verify(connection).prepareStatement(SELECT_NAMED_PARAMETERS_PARSED); + verify(preparedStatement).setObject(1, 1, Types.DECIMAL); + verify(preparedStatement).setString(2, "UK"); + verify(resultSet).close(); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + public void testQueryWithRowMapperNoParameters() throws SQLException { + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt("id")).willReturn(1); + given(resultSet.getString("forename")).willReturn("rod"); + + Set customers = client.sql(SELECT_NO_PARAMETERS).query( + (rs, rownum) -> { + Customer cust = new Customer(); + cust.setId(rs.getInt(COLUMN_NAMES[0])); + cust.setForename(rs.getString(COLUMN_NAMES[1])); + return cust; + }).set(); + + assertThat(customers).hasSize(1); + Customer cust = customers.iterator().next(); + assertThat(cust.getId()).as("Customer id was assigned correctly").isEqualTo(1); + assertThat(cust.getForename()).as("Customer forename was assigned correctly").isEqualTo("rod"); + verify(connection).prepareStatement(SELECT_NO_PARAMETERS); + verify(resultSet).close(); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + public void testQueryForObjectWithRowMapper() throws SQLException { + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt("id")).willReturn(1); + given(resultSet.getString("forename")).willReturn("rod"); + + params.put("id", new SqlParameterValue(Types.DECIMAL, 1)); + params.put("country", "UK"); + + Customer cust = client.sql(SELECT_NAMED_PARAMETERS).params(params).query( + (rs, rownum) -> { + Customer cust1 = new Customer(); + cust1.setId(rs.getInt(COLUMN_NAMES[0])); + cust1.setForename(rs.getString(COLUMN_NAMES[1])); + return cust1; + }).single(); + + assertThat(cust.getId()).as("Customer id was assigned correctly").isEqualTo(1); + assertThat(cust.getForename()).as("Customer forename was assigned correctly").isEqualTo("rod"); + verify(connection).prepareStatement(SELECT_NAMED_PARAMETERS_PARSED); + verify(preparedStatement).setObject(1, 1, Types.DECIMAL); + verify(preparedStatement).setString(2, "UK"); + verify(resultSet).close(); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + public void testQueryForStreamWithRowMapper() throws SQLException { + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt("id")).willReturn(1); + given(resultSet.getString("forename")).willReturn("rod"); + + params.put("id", new SqlParameterValue(Types.DECIMAL, 1)); + params.put("country", "UK"); + AtomicInteger count = new AtomicInteger(); + + try (Stream s = client.sql(SELECT_NAMED_PARAMETERS).params(params).query( + (rs, rownum) -> { + Customer cust1 = new Customer(); + cust1.setId(rs.getInt(COLUMN_NAMES[0])); + cust1.setForename(rs.getString(COLUMN_NAMES[1])); + return cust1; + }).stream()) { + s.forEach(cust -> { + count.incrementAndGet(); + assertThat(cust.getId()).as("Customer id was assigned correctly").isEqualTo(1); + assertThat(cust.getForename()).as("Customer forename was assigned correctly").isEqualTo("rod"); + }); + } + + assertThat(count.get()).isEqualTo(1); + verify(connection).prepareStatement(SELECT_NAMED_PARAMETERS_PARSED); + verify(preparedStatement).setObject(1, 1, Types.DECIMAL); + verify(preparedStatement).setString(2, "UK"); + verify(resultSet).close(); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + public void testUpdate() throws SQLException { + given(preparedStatement.executeUpdate()).willReturn(1); + + params.put("perfId", 1); + params.put("priceId", 1); + int rowsAffected = client.sql(UPDATE_NAMED_PARAMETERS).params(params).update(); + + assertThat(rowsAffected).isEqualTo(1); + verify(connection).prepareStatement(UPDATE_NAMED_PARAMETERS_PARSED); + verify(preparedStatement).setObject(1, 1); + verify(preparedStatement).setObject(2, 1); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + public void testUpdateWithTypedParameters() throws SQLException { + given(preparedStatement.executeUpdate()).willReturn(1); + + params.put("perfId", new SqlParameterValue(Types.DECIMAL, 1)); + params.put("priceId", new SqlParameterValue(Types.INTEGER, 1)); + int rowsAffected = client.sql(UPDATE_NAMED_PARAMETERS).params(params).update(); + + assertThat(rowsAffected).isEqualTo(1); + verify(connection).prepareStatement(UPDATE_NAMED_PARAMETERS_PARSED); + verify(preparedStatement).setObject(1, 1, Types.DECIMAL); + verify(preparedStatement).setObject(2, 1, Types.INTEGER); + verify(preparedStatement).close(); + verify(connection).close(); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientQueryTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientQueryTests.java new file mode 100644 index 00000000000..b0a14fc45bb --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientQueryTests.java @@ -0,0 +1,476 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * 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 + * + * https://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 org.springframework.jdbc.core.simple; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.Types; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * @author Juergen Hoeller + * @since 6.1 + */ +public class JdbcClientQueryTests { + + private Connection connection = mock(); + + private DataSource dataSource = mock(); + + private PreparedStatement preparedStatement = mock(); + + private ResultSet resultSet = mock(); + + private ResultSetMetaData resultSetMetaData = mock(); + + private JdbcClient client = JdbcClient.create(dataSource); + + + @BeforeEach + public void setup() throws Exception { + given(dataSource.getConnection()).willReturn(connection); + given(resultSetMetaData.getColumnCount()).willReturn(1); + given(resultSetMetaData.getColumnLabel(1)).willReturn("age"); + given(connection.prepareStatement(anyString())).willReturn(preparedStatement); + given(preparedStatement.executeQuery()).willReturn(resultSet); + } + + @AfterEach + public void verifyClose() throws Exception { + verify(preparedStatement).close(); + verify(resultSet).close(); + verify(connection).close(); + } + + + // Indexed parameters + + @Test + public void testQueryForListWithIndexedParam() throws Exception { + given(resultSet.getMetaData()).willReturn(resultSetMetaData); + given(resultSet.next()).willReturn(true, true, false); + given(resultSet.getObject(1)).willReturn(11, 12); + + List> li = client.sql("SELECT AGE FROM CUSTMR WHERE ID < ?") + .param(3).query().listOfRows(); + + assertThat(li.size()).as("All rows returned").isEqualTo(2); + assertThat(li.get(0).get("age")).as("First row is Integer").isEqualTo(11); + assertThat(li.get(1).get("age")).as("Second row is Integer").isEqualTo(12); + + verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID < ?"); + verify(preparedStatement).setObject(1, 3); + } + + @Test + public void testQueryForListWithIndexedParamAndEmptyResult() throws Exception { + given(resultSet.next()).willReturn(false); + + List> li = client.sql("SELECT AGE FROM CUSTMR WHERE ID < ?") + .param(3).query().listOfRows(); + + assertThat(li.size()).as("All rows returned").isEqualTo(0); + verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID < ?"); + verify(preparedStatement).setObject(1, 3); + } + + @Test + public void testQueryForListWithIndexedParamAndSingleRowAndColumn() throws Exception { + given(resultSet.getMetaData()).willReturn(resultSetMetaData); + given(resultSet.next()).willReturn(true, false); + given(resultSet.getObject(1)).willReturn(11); + + List> li = client.sql("SELECT AGE FROM CUSTMR WHERE ID < ?") + .param(3).query().listOfRows(); + + assertThat(li.size()).as("All rows returned").isEqualTo(1); + assertThat(li.get(0).get("age")).as("First row is Integer").isEqualTo(11); + verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID < ?"); + verify(preparedStatement).setObject(1, 3); + } + + @Test + public void testQueryForListWithIndexedParamAndIntegerElementAndSingleRowAndColumn() throws Exception { + given(resultSet.getMetaData()).willReturn(resultSetMetaData); + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt(1)).willReturn(11); + + List li = client.sql("SELECT AGE FROM CUSTMR WHERE ID < ?") + .param(1, 3) + .query().singleColumn(Integer.class); + + assertThat(li.size()).as("All rows returned").isEqualTo(1); + assertThat(li.get(0)).as("First row is Integer").isEqualTo(11); + verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID < ?"); + verify(preparedStatement).setObject(1, 3); + } + + @Test + public void testQueryForMapWithIndexedParamAndSingleRowAndColumn() throws Exception { + given(resultSet.getMetaData()).willReturn(resultSetMetaData); + given(resultSet.next()).willReturn(true, false); + given(resultSet.getObject(1)).willReturn(11); + + Map map = client.sql("SELECT AGE FROM CUSTMR WHERE ID < ?") + .param(1, 3) + .query().singleRow(); + + assertThat(map.get("age")).as("Row is Integer").isEqualTo(11); + verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID < ?"); + verify(preparedStatement).setObject(1, 3); + } + + @Test + public void testQueryForObjectWithIndexedParamAndRowMapper() throws Exception { + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt(1)).willReturn(22); + + Integer value = client.sql("SELECT AGE FROM CUSTMR WHERE ID = ?") + .param(1, 3) + .query((rs, rowNum) -> rs.getInt(1)).single(); + + assertThat(value).isEqualTo(22); + verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID = ?"); + verify(preparedStatement).setObject(1, 3); + } + + @Test + public void testQueryForOptionalWithIndexedParamAndRowMapper() throws Exception { + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt(1)).willReturn(22); + + Optional value = client.sql("SELECT AGE FROM CUSTMR WHERE ID = ?") + .param(1, 3) + .query((rs, rowNum) -> rs.getInt(1)).optional(); + + assertThat(value.get()).isEqualTo(22); + verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID = ?"); + verify(preparedStatement).setObject(1, 3); + } + + @Test + public void testQueryForObjectWithIndexedParamAndInteger() throws Exception { + given(resultSet.getMetaData()).willReturn(resultSetMetaData); + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt(1)).willReturn(22); + + Integer value = client.sql("SELECT AGE FROM CUSTMR WHERE ID = ?") + .param(1, 3) + .query().singleValue(Integer.class); + + assertThat(value).isEqualTo(22); + verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID = ?"); + verify(preparedStatement).setObject(1, 3); + } + + @Test + public void testQueryForIntWithIndexedParam() throws Exception { + given(resultSet.getMetaData()).willReturn(resultSetMetaData); + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt(1)).willReturn(22); + + int i = client.sql("SELECT AGE FROM CUSTMR WHERE ID = ?") + .param(1, 3) + .query().singleValue(Integer.class); + + assertThat(i).as("Return of an int").isEqualTo(22); + verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID = ?"); + verify(preparedStatement).setObject(1, 3); + } + + + // Named parameters + + @Test + public void testQueryForListWithNamedParam() throws Exception { + given(resultSet.getMetaData()).willReturn(resultSetMetaData); + given(resultSet.next()).willReturn(true, true, false); + given(resultSet.getObject(1)).willReturn(11, 12); + + List> li = client.sql("SELECT AGE FROM CUSTMR WHERE ID < :id") + .param("id", 3) + .query().listOfRows(); + + assertThat(li.size()).as("All rows returned").isEqualTo(2); + assertThat(li.get(0).get("age")).as("First row is Integer").isEqualTo(11); + assertThat(li.get(1).get("age")).as("Second row is Integer").isEqualTo(12); + + verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID < ?"); + verify(preparedStatement).setObject(1, 3); + } + + @Test + public void testQueryForListWithNamedParamAndEmptyResult() throws Exception { + given(resultSet.next()).willReturn(false); + + List> li = client.sql("SELECT AGE FROM CUSTMR WHERE ID < :id") + .param("id", 3) + .query().listOfRows(); + + assertThat(li.size()).as("All rows returned").isEqualTo(0); + verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID < ?"); + verify(preparedStatement).setObject(1, 3); + } + + @Test + public void testQueryForListWithNamedParamAndSingleRowAndColumn() throws Exception { + given(resultSet.getMetaData()).willReturn(resultSetMetaData); + given(resultSet.next()).willReturn(true, false); + given(resultSet.getObject(1)).willReturn(11); + + List> li = client.sql("SELECT AGE FROM CUSTMR WHERE ID < :id") + .param("id", 3) + .query().listOfRows(); + + assertThat(li.size()).as("All rows returned").isEqualTo(1); + assertThat(li.get(0).get("age")).as("First row is Integer").isEqualTo(11); + verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID < ?"); + verify(preparedStatement).setObject(1, 3); + } + + @Test + public void testQueryForListWithNamedParamAndIntegerElementAndSingleRowAndColumn() throws Exception { + given(resultSet.getMetaData()).willReturn(resultSetMetaData); + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt(1)).willReturn(11); + + List li = client.sql("SELECT AGE FROM CUSTMR WHERE ID < :id") + .param("id", 3) + .query().singleColumn(Integer.class); + + assertThat(li.size()).as("All rows returned").isEqualTo(1); + assertThat(li.get(0)).as("First row is Integer").isEqualTo(11); + verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID < ?"); + verify(preparedStatement).setObject(1, 3); + } + + @Test + public void testQueryForMapWithNamedParamAndSingleRowAndColumn() throws Exception { + given(resultSet.getMetaData()).willReturn(resultSetMetaData); + given(resultSet.next()).willReturn(true, false); + given(resultSet.getObject(1)).willReturn(11); + + Map map = client.sql("SELECT AGE FROM CUSTMR WHERE ID < :id") + .param("id", 3) + .query().singleRow(); + + assertThat(map.get("age")).as("Row is Integer").isEqualTo(11); + verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID < ?"); + verify(preparedStatement).setObject(1, 3); + } + + @Test + public void testQueryForObjectWithNamedParamAndRowMapper() throws Exception { + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt(1)).willReturn(22); + + Integer value = client.sql("SELECT AGE FROM CUSTMR WHERE ID = :id") + .param("id", 3) + .query((rs, rowNum) -> rs.getInt(1)) + .single(); + + assertThat(value).isEqualTo(22); + verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID = ?"); + verify(preparedStatement).setObject(1, 3); + } + + @Test + public void testQueryForObjectWithNamedParamAndInteger() throws Exception { + given(resultSet.getMetaData()).willReturn(resultSetMetaData); + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt(1)).willReturn(22); + + Integer value = client.sql("SELECT AGE FROM CUSTMR WHERE ID = :id") + .param("id", 3) + .query().singleValue(Integer.class); + + assertThat(value).isEqualTo(22); + verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID = ?"); + verify(preparedStatement).setObject(1, 3); + } + + @Test + public void testQueryForObjectWithNamedParamAndList() throws Exception { + given(resultSet.getMetaData()).willReturn(resultSetMetaData); + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt(1)).willReturn(22); + + Integer value = client.sql("SELECT AGE FROM CUSTMR WHERE ID IN (:ids)") + .param("ids", Arrays.asList(3, 4)) + .query().singleValue(Integer.class); + + assertThat(value).isEqualTo(22); + verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID IN (?, ?)"); + verify(preparedStatement).setObject(1, 3); + } + + @Test + public void testQueryForObjectWithNamedParamAndListOfExpressionLists() throws Exception { + given(resultSet.getMetaData()).willReturn(resultSetMetaData); + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt(1)).willReturn(22); + + List l1 = new ArrayList<>(); + l1.add(new Object[] {3, "Rod"}); + l1.add(new Object[] {4, "Juergen"}); + Integer value = client.sql("SELECT AGE FROM CUSTMR WHERE (ID, NAME) IN (:multiExpressionList)") + .param("multiExpressionList", l1) + .query().singleValue(Integer.class); + + assertThat(value).isEqualTo(22); + verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE (ID, NAME) IN ((?, ?), (?, ?))"); + verify(preparedStatement).setObject(1, 3); + } + + @Test + public void testQueryForIntWithNamedParam() throws Exception { + given(resultSet.getMetaData()).willReturn(resultSetMetaData); + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt(1)).willReturn(22); + + int i = client.sql("SELECT AGE FROM CUSTMR WHERE ID = :id") + .param("id", 3) + .query().singleValue(Integer.class); + + assertThat(i).as("Return of an int").isEqualTo(22); + verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID = ?"); + verify(preparedStatement).setObject(1, 3); + } + + @Test + public void testQueryForLongWithParamBean() throws Exception { + given(resultSet.getMetaData()).willReturn(resultSetMetaData); + given(resultSet.next()).willReturn(true, false); + given(resultSet.getLong(1)).willReturn(87L); + + long l = client.sql("SELECT AGE FROM CUSTMR WHERE ID = :id") + .paramSource(new ParameterBean(3)) + .query().singleValue(Long.class); + + assertThat(l).as("Return of a long").isEqualTo(87); + verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID = ?"); + verify(preparedStatement).setObject(1, 3, Types.INTEGER); + } + + @Test + public void testQueryForLongWithParamBeanWithCollection() throws Exception { + given(resultSet.getMetaData()).willReturn(resultSetMetaData); + given(resultSet.next()).willReturn(true, false); + given(resultSet.getLong(1)).willReturn(87L); + + long l = client.sql("SELECT AGE FROM CUSTMR WHERE ID IN (:ids)") + .paramSource(new ParameterCollectionBean(3, 5)) + .query().singleValue(Long.class); + + assertThat(l).as("Return of a long").isEqualTo(87); + verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID IN (?, ?)"); + verify(preparedStatement).setObject(1, 3); + verify(preparedStatement).setObject(2, 5); + } + + @Test + public void testQueryForLongWithParamRecord() throws Exception { + given(resultSet.getMetaData()).willReturn(resultSetMetaData); + given(resultSet.next()).willReturn(true, false); + given(resultSet.getLong(1)).willReturn(87L); + + long l = client.sql("SELECT AGE FROM CUSTMR WHERE ID = :id") + .paramSource(new ParameterRecord(3)) + .query().singleValue(Long.class); + + assertThat(l).as("Return of a long").isEqualTo(87); + verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID = ?"); + verify(preparedStatement).setObject(1, 3, Types.INTEGER); + } + + @Test + public void testQueryForLongWithParamFieldHolder() throws Exception { + given(resultSet.getMetaData()).willReturn(resultSetMetaData); + given(resultSet.next()).willReturn(true, false); + given(resultSet.getLong(1)).willReturn(87L); + + long l = client.sql("SELECT AGE FROM CUSTMR WHERE ID = :id") + .paramSource(new ParameterFieldHolder(3)) + .query().singleValue(Long.class); + + assertThat(l).as("Return of a long").isEqualTo(87); + verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID = ?"); + verify(preparedStatement).setObject(1, 3, Types.INTEGER); + } + + + static class ParameterBean { + + private final int id; + + public ParameterBean(int id) { + this.id = id; + } + + public int getId() { + return id; + } + } + + + static class ParameterCollectionBean { + + private final Collection ids; + + public ParameterCollectionBean(Integer... ids) { + this.ids = Arrays.asList(ids); + } + + public Collection getIds() { + return ids; + } + } + + + record ParameterRecord(int id) { + } + + + static class ParameterFieldHolder { + + public ParameterFieldHolder(int id) { + this.id = id; + } + + public int id; + } + +}