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 8f8aaab3516..a111549fbed 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 @@ -196,6 +196,26 @@ public class JdbcTemplate extends JdbcAccessor implements JdbcOperations { afterPropertiesSet(); } + /** + * Copy constructor for a derived JdbcTemplate. + * @param original the original template to copy from + * @since 7.0 + */ + public JdbcTemplate(JdbcAccessor original) { + setDataSource(original.getDataSource()); + setExceptionTranslator(original.getExceptionTranslator()); + setLazyInit(original.isLazyInit()); + if (original instanceof JdbcTemplate originalTemplate) { + setIgnoreWarnings(originalTemplate.isIgnoreWarnings()); + setFetchSize(originalTemplate.getFetchSize()); + setMaxRows(originalTemplate.getMaxRows()); + setQueryTimeout(originalTemplate.getQueryTimeout()); + setSkipResultsProcessing(originalTemplate.isSkipResultsProcessing()); + setSkipUndeclaredResults(originalTemplate.isSkipUndeclaredResults()); + setResultsMapCaseInsensitive(originalTemplate.isResultsMapCaseInsensitive()); + } + } + /** * Set whether we want to ignore JDBC statement warnings ({@link SQLWarning}). @@ -264,7 +284,7 @@ public class JdbcTemplate extends JdbcAccessor implements JdbcOperations { } /** - * Set the query timeout for statements that this JdbcTemplate executes. + * Set the query timeout (seconds) for statements that this JdbcTemplate executes. *

Default is -1, indicating to use the JDBC driver's default * (i.e. to not pass a specific query timeout setting on the driver). *

Note: Any timeout specified here will be overridden by the remaining @@ -277,7 +297,7 @@ public class JdbcTemplate extends JdbcAccessor implements JdbcOperations { } /** - * Return the query timeout for statements that this JdbcTemplate executes. + * Return the query timeout (seconds) for statements that this JdbcTemplate executes. */ public int getQueryTimeout() { return this.queryTimeout; 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 fa07fec74bc..f41cd0eb7b1 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 @@ -87,8 +87,7 @@ public class NamedParameterJdbcTemplate implements NamedParameterJdbcOperations private final JdbcOperations classicJdbcTemplate; /** Cache of original SQL String to ParsedSql representation. */ - private volatile ConcurrentLruCache parsedSqlCache = - new ConcurrentLruCache<>(DEFAULT_CACHE_LIMIT, NamedParameterUtils::parseSqlStatement); + private volatile ConcurrentLruCache parsedSqlCache; /** @@ -97,8 +96,7 @@ public class NamedParameterJdbcTemplate implements NamedParameterJdbcOperations * @param dataSource the JDBC DataSource to access */ public NamedParameterJdbcTemplate(DataSource dataSource) { - Assert.notNull(dataSource, "DataSource must not be null"); - this.classicJdbcTemplate = new JdbcTemplate(dataSource); + this(new JdbcTemplate(dataSource)); } /** @@ -109,6 +107,19 @@ public class NamedParameterJdbcTemplate implements NamedParameterJdbcOperations public NamedParameterJdbcTemplate(JdbcOperations classicJdbcTemplate) { Assert.notNull(classicJdbcTemplate, "JdbcTemplate must not be null"); this.classicJdbcTemplate = classicJdbcTemplate; + this.parsedSqlCache = new ConcurrentLruCache<>(DEFAULT_CACHE_LIMIT, NamedParameterUtils::parseSqlStatement); + } + + /** + * Copy constructor for a derived NamedParameterJdbcTemplate. + * @param original the original NamedParameterJdbcTemplate to copy from + * @param classicJdbcTemplate the actual JdbcTemplate delegate to use + * @since 7.0 + */ + public NamedParameterJdbcTemplate(NamedParameterJdbcTemplate original, JdbcTemplate classicJdbcTemplate) { + Assert.notNull(classicJdbcTemplate, "JdbcTemplate must not be null"); + this.classicJdbcTemplate = classicJdbcTemplate; + this.parsedSqlCache = original.parsedSqlCache; } 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 index d9e8df8398c..8a91191b63b 100644 --- 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 @@ -45,6 +45,7 @@ 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.JdbcAccessor; import org.springframework.jdbc.support.KeyHolder; import org.springframework.jdbc.support.rowset.SqlRowSet; import org.springframework.util.Assert; @@ -62,8 +63,6 @@ import org.springframework.util.Assert; */ final class DefaultJdbcClient implements JdbcClient { - private final JdbcOperations classicOps; - private final NamedParameterJdbcOperations namedParamOps; private final ConversionService conversionService; @@ -81,7 +80,6 @@ final class DefaultJdbcClient implements JdbcClient { public DefaultJdbcClient(NamedParameterJdbcOperations jdbcTemplate, @Nullable ConversionService conversionService) { Assert.notNull(jdbcTemplate, "JdbcTemplate must not be null"); - this.classicOps = jdbcTemplate.getJdbcOperations(); this.namedParamOps = jdbcTemplate; this.conversionService = (conversionService != null ? conversionService : DefaultConversionService.getSharedInstance()); @@ -90,7 +88,7 @@ final class DefaultJdbcClient implements JdbcClient { @Override public StatementSpec sql(String sql) { - return new DefaultStatementSpec(sql); + return new DefaultStatementSpec(sql, this.namedParamOps); } @@ -98,14 +96,55 @@ final class DefaultJdbcClient implements JdbcClient { private final String sql; - private final List indexedParams = new ArrayList<>(); + private JdbcOperations classicOps; + + private NamedParameterJdbcOperations namedParamOps; + + private @Nullable JdbcTemplate customTemplate; + + private final List<@Nullable Object> indexedParams = new ArrayList<>(); private final MapSqlParameterSource namedParams = new MapSqlParameterSource(); private SqlParameterSource namedParamSource = this.namedParams; - public DefaultStatementSpec(String sql) { + public DefaultStatementSpec(String sql, NamedParameterJdbcOperations namedParamOps) { this.sql = sql; + this.classicOps = namedParamOps.getJdbcOperations(); + this.namedParamOps = namedParamOps; + } + + private JdbcTemplate enforceCustomTemplate() { + if (this.customTemplate == null) { + if (!(this.classicOps instanceof JdbcAccessor original)) { + throw new IllegalStateException( + "Needs to be bound to a JdbcAccessor for custom settings support: " + this.classicOps); + } + this.customTemplate = new JdbcTemplate(original); + this.classicOps = this.customTemplate; + this.namedParamOps = (this.namedParamOps instanceof NamedParameterJdbcTemplate originalNamedParam ? + new NamedParameterJdbcTemplate(originalNamedParam, this.customTemplate) : + new NamedParameterJdbcTemplate(this.customTemplate)); + } + return this.customTemplate; + } + + @Override + public StatementSpec withFetchSize(int fetchSize) { + enforceCustomTemplate().setFetchSize(fetchSize); + return this; + } + + @Override + public StatementSpec withMaxRows(int maxRows) { + enforceCustomTemplate().setMaxRows(maxRows); + return this; + } + + @Override + public StatementSpec withQueryTimeout(int queryTimeout) { + enforceCustomTemplate().setQueryTimeout(queryTimeout); + return this; } @Override @@ -220,18 +259,18 @@ final class DefaultJdbcClient implements JdbcClient { @Override public void query(RowCallbackHandler rch) { if (useNamedParams()) { - namedParamOps.query(this.sql, this.namedParamSource, rch); + this.namedParamOps.query(this.sql, this.namedParamSource, rch); } else { - classicOps.query(statementCreatorForIndexedParams(), rch); + this.classicOps.query(statementCreatorForIndexedParams(), rch); } } @Override public T query(ResultSetExtractor rse) { T result = (useNamedParams() ? - namedParamOps.query(this.sql, this.namedParamSource, rse) : - classicOps.query(statementCreatorForIndexedParams(), rse)); + this.namedParamOps.query(this.sql, this.namedParamSource, rse) : + this.classicOps.query(statementCreatorForIndexedParams(), rse)); Assert.state(result != null, "No result from ResultSetExtractor"); return result; } @@ -239,22 +278,22 @@ final class DefaultJdbcClient implements JdbcClient { @Override public int update() { return (useNamedParams() ? - namedParamOps.update(this.sql, this.namedParamSource) : - classicOps.update(statementCreatorForIndexedParams())); + this.namedParamOps.update(this.sql, this.namedParamSource) : + this.classicOps.update(statementCreatorForIndexedParams())); } @Override public int update(KeyHolder generatedKeyHolder) { return (useNamedParams() ? - namedParamOps.update(this.sql, this.namedParamSource, generatedKeyHolder) : - classicOps.update(statementCreatorForIndexedParamsWithKeys(null), generatedKeyHolder)); + this.namedParamOps.update(this.sql, this.namedParamSource, generatedKeyHolder) : + this.classicOps.update(statementCreatorForIndexedParamsWithKeys(null), generatedKeyHolder)); } @Override public int update(KeyHolder generatedKeyHolder, String... keyColumnNames) { return (useNamedParams() ? - namedParamOps.update(this.sql, this.namedParamSource, generatedKeyHolder, keyColumnNames) : - classicOps.update(statementCreatorForIndexedParamsWithKeys(keyColumnNames), generatedKeyHolder)); + this.namedParamOps.update(this.sql, this.namedParamSource, generatedKeyHolder, keyColumnNames) : + this.classicOps.update(statementCreatorForIndexedParamsWithKeys(keyColumnNames), generatedKeyHolder)); } private boolean useNamedParams() { 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 index 53f0e055dad..7c512790e2a 100644 --- 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 @@ -132,6 +132,30 @@ public interface JdbcClient { */ interface StatementSpec { + /** + * Apply the given fetch size to any subsequent query statement. + * @param fetchSize the fetch size + * @since 7.0 + * @see org.springframework.jdbc.core.JdbcTemplate#setFetchSize + */ + StatementSpec withFetchSize(int fetchSize); + + /** + * Apply the given maximum number of rows to any subsequent query statement. + * @param maxRows the maximum number of rows + * @since 7.0 + * @see org.springframework.jdbc.core.JdbcTemplate#setMaxRows + */ + StatementSpec withMaxRows(int maxRows); + + /** + * Apply the given query timeout to any subsequent query statement. + * @param queryTimeout the query timeout in seconds + * @since 7.0 + * @see org.springframework.jdbc.core.JdbcTemplate#setQueryTimeout + */ + StatementSpec withQueryTimeout(int queryTimeout); + /** * Bind a positional JDBC statement parameter for "?" placeholder resolution * by implicit order of parameter value registration. diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientIntegrationTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientIntegrationTests.java index fd4daf2064d..a8580bde2af 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientIntegrationTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientIntegrationTests.java @@ -195,6 +195,19 @@ class JdbcClientIntegrationTests { assertResults(users); } + @Test + void selectWithReusedNamedParameterAndMaxRows() { + List users = jdbcClient.sql(QUERY1) + .withFetchSize(1) + .withMaxRows(1) + .withQueryTimeout(1) + .param("name", "John") + .query(User.class) + .list(); + + assertSingleResult(users); + } + @Test void selectWithReusedNamedParameterList() { List users = jdbcClient.sql(QUERY2) @@ -215,15 +228,31 @@ class JdbcClientIntegrationTests { assertResults(users); } + @Test + void selectWithReusedNamedParameterListAndMaxRows() { + List users = jdbcClient.sql(QUERY2) + .withFetchSize(1) + .withMaxRows(1) + .withQueryTimeout(1) + .paramSource(new Names(List.of("John", "Bogus"))) + .query(User.class) + .list(); + + assertSingleResult(users); + } private static void assertResults(List users) { assertThat(users).containsExactly(new User(2, "John", "John"), new User(3, "John", "Smith")); } + private static void assertSingleResult(List users) { + assertThat(users).containsExactly(new User(2, "John", "John")); + } + + record Name(String name) {} record Names(List names) {} - }