diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d46635fb..9a61b8b89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ worked to make these changes as minimal as possible. **Potentially Breaking Changes:** +- If you use this library with MyBatis' Spring Batch integration, you will need to make changes as we have + refactored that support to be more flexible. Please see the + [Spring Batch](https://mybatis.org/mybatis-dynamic-sql/docs/springBatch.html) documentation page to see the new usage + details. - If you have created any custom implementations of `SortSpecification`, you will need to update those implementations due to a new rendering strategy for ORDER BY phrases. The old methods `isDescending` and `orderByName` are removed in favor of a new method `renderForOrderBy` diff --git a/pom.xml b/pom.xml index b49e7f205..8f73f520e 100644 --- a/pom.xml +++ b/pom.xml @@ -58,6 +58,8 @@ 17 17 + 17 + 17 5.11.3 5.1.2 diff --git a/src/main/java/org/mybatis/dynamic/sql/delete/render/DeleteRenderer.java b/src/main/java/org/mybatis/dynamic/sql/delete/render/DeleteRenderer.java index 8a9c945d4..fa4cd0d28 100644 --- a/src/main/java/org/mybatis/dynamic/sql/delete/render/DeleteRenderer.java +++ b/src/main/java/org/mybatis/dynamic/sql/delete/render/DeleteRenderer.java @@ -84,7 +84,7 @@ private Optional calculateLimitClause() { } private FragmentAndParameters renderLimitClause(Long limit) { - RenderedParameterInfo parameterInfo = renderingContext.calculateParameterInfo(); + RenderedParameterInfo parameterInfo = renderingContext.calculateLimitParameterInfo(); return FragmentAndParameters.withFragment("limit " + parameterInfo.renderedPlaceHolder()) //$NON-NLS-1$ .withParameter(parameterInfo.parameterMapKey(), limit) diff --git a/src/main/java/org/mybatis/dynamic/sql/render/RenderingContext.java b/src/main/java/org/mybatis/dynamic/sql/render/RenderingContext.java index bc953f847..553ecd090 100644 --- a/src/main/java/org/mybatis/dynamic/sql/render/RenderingContext.java +++ b/src/main/java/org/mybatis/dynamic/sql/render/RenderingContext.java @@ -57,18 +57,27 @@ private String nextMapKey() { return renderingStrategy.formatParameterMapKey(sequence); } - private String renderedPlaceHolder(String mapKey) { - return renderingStrategy.getFormattedJdbcPlaceholder(calculatedParameterName, mapKey); - } - private String renderedPlaceHolder(String mapKey, BindableColumn column) { return column.renderingStrategy().orElse(renderingStrategy) .getFormattedJdbcPlaceholder(column, calculatedParameterName, mapKey); } - public RenderedParameterInfo calculateParameterInfo() { - String mapKey = nextMapKey(); - return new RenderedParameterInfo(mapKey, renderedPlaceHolder(mapKey)); + public RenderedParameterInfo calculateFetchFirstRowsParameterInfo() { + String mapKey = renderingStrategy.formatParameterMapKeyForFetchFirstRows(sequence); + return new RenderedParameterInfo(mapKey, + renderingStrategy.getFormattedJdbcPlaceholderForPagingParameters(calculatedParameterName, mapKey)); + } + + public RenderedParameterInfo calculateLimitParameterInfo() { + String mapKey = renderingStrategy.formatParameterMapKeyForLimit(sequence); + return new RenderedParameterInfo(mapKey, + renderingStrategy.getFormattedJdbcPlaceholderForPagingParameters(calculatedParameterName, mapKey)); + } + + public RenderedParameterInfo calculateOffsetParameterInfo() { + String mapKey = renderingStrategy.formatParameterMapKeyForOffset(sequence); + return new RenderedParameterInfo(mapKey, + renderingStrategy.getFormattedJdbcPlaceholderForPagingParameters(calculatedParameterName, mapKey)); } public RenderedParameterInfo calculateParameterInfo(BindableColumn column) { diff --git a/src/main/java/org/mybatis/dynamic/sql/render/RenderingStrategy.java b/src/main/java/org/mybatis/dynamic/sql/render/RenderingStrategy.java index 70b369c73..6beb7bc24 100644 --- a/src/main/java/org/mybatis/dynamic/sql/render/RenderingStrategy.java +++ b/src/main/java/org/mybatis/dynamic/sql/render/RenderingStrategy.java @@ -36,10 +36,55 @@ public abstract class RenderingStrategy { public static final String DEFAULT_PARAMETER_PREFIX = "parameters"; //$NON-NLS-1$ + /** + * Generate a unique key that can be used to place a parameter value in the parameter map. + * + * @param sequence a sequence for calculating a unique value + * @return a key used to place the parameter value in the parameter map + */ public String formatParameterMapKey(AtomicInteger sequence) { return "p" + sequence.getAndIncrement(); //$NON-NLS-1$ } + /** + * Return a parameter map key intended as a parameter for a fetch first query. + * + *

By default, this parameter is treated the same as any other. This method is a hook to support + * MyBatis Spring Batch. + * + * @param sequence a sequence for calculating a unique value + * @return a key used to place the parameter value in the parameter map + */ + public String formatParameterMapKeyForFetchFirstRows(AtomicInteger sequence) { + return formatParameterMapKey(sequence); + } + + /** + * Return a parameter map key intended as a parameter for a limit query. + * + *

By default, this parameter is treated the same as any other. This method is a hook to support + * MyBatis Spring Batch. + * + * @param sequence a sequence for calculating a unique value + * @return a key used to place the parameter value in the parameter map + */ + public String formatParameterMapKeyForLimit(AtomicInteger sequence) { + return formatParameterMapKey(sequence); + } + + /** + * Return a parameter map key intended as a parameter for a query offset. + * + *

By default, this parameter is treated the same as any other. This method is a hook to support + * MyBatis Spring Batch. + * + * @param sequence a sequence for calculating a unique value + * @return a key used to place the parameter value in the parameter map + */ + public String formatParameterMapKeyForOffset(AtomicInteger sequence) { + return formatParameterMapKey(sequence); + } + /** * This method generates a binding for a parameter to a placeholder in a generated SQL statement. * @@ -78,6 +123,23 @@ public String formatParameterMapKey(AtomicInteger sequence) { */ public abstract String getFormattedJdbcPlaceholder(String prefix, String parameterName); + /** + * This method generates a binding for a parameter to a placeholder in a generated SQL statement. + * + *

This method is used to generate bindings for limit, offset, and fetch first parameters. By default, these + * parameters are treated the same as any other. This method supports MyBatis Spring Batch integration where the + * parameter keys have predefined values and need special handling. + * + * @param prefix parameter prefix used for locating the parameters in a SQL provider object. Typically, will be + * {@link RenderingStrategy#DEFAULT_PARAMETER_PREFIX}. This is ignored for Spring. + * @param parameterName name of the parameter. Typically generated by calling + * {@link RenderingStrategy#formatParameterMapKey(AtomicInteger)} + * @return the generated binding + */ + public String getFormattedJdbcPlaceholderForPagingParameters(String prefix, String parameterName) { + return getFormattedJdbcPlaceholder(prefix, parameterName); + } + /** * This method generates a binding for a parameter to a placeholder in a row based insert statement. * diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/FetchFirstPagingModelRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/FetchFirstPagingModelRenderer.java index fcd62264b..0ad493620 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/render/FetchFirstPagingModelRenderer.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/FetchFirstPagingModelRenderer.java @@ -51,30 +51,30 @@ private FragmentAndParameters renderFetchFirstRowsOnly() { } private FragmentAndParameters renderFetchFirstRowsOnly(Long fetchFirstRows) { - RenderedParameterInfo parameterInfo = renderingContext.calculateParameterInfo(); + RenderedParameterInfo fetchFirstParameterInfo = renderingContext.calculateFetchFirstRowsParameterInfo(); return FragmentAndParameters - .withFragment("fetch first " + parameterInfo.renderedPlaceHolder() //$NON-NLS-1$ + .withFragment("fetch first " + fetchFirstParameterInfo.renderedPlaceHolder() //$NON-NLS-1$ + " rows only") //$NON-NLS-1$ - .withParameter(parameterInfo.parameterMapKey(), fetchFirstRows) + .withParameter(fetchFirstParameterInfo.parameterMapKey(), fetchFirstRows) .build(); } private FragmentAndParameters renderOffsetOnly(Long offset) { - RenderedParameterInfo parameterInfo = renderingContext.calculateParameterInfo(); - return FragmentAndParameters.withFragment("offset " + parameterInfo.renderedPlaceHolder() //$NON-NLS-1$ + RenderedParameterInfo offsetParameterInfo = renderingContext.calculateOffsetParameterInfo(); + return FragmentAndParameters.withFragment("offset " + offsetParameterInfo.renderedPlaceHolder() //$NON-NLS-1$ + " rows") //$NON-NLS-1$ - .withParameter(parameterInfo.parameterMapKey(), offset) + .withParameter(offsetParameterInfo.parameterMapKey(), offset) .build(); } private FragmentAndParameters renderOffsetAndFetchFirstRows(Long offset, Long fetchFirstRows) { - RenderedParameterInfo parameterInfo1 = renderingContext.calculateParameterInfo(); - RenderedParameterInfo parameterInfo2 = renderingContext.calculateParameterInfo(); - return FragmentAndParameters.withFragment("offset " + parameterInfo1.renderedPlaceHolder() //$NON-NLS-1$ - + " rows fetch first " + parameterInfo2.renderedPlaceHolder() //$NON-NLS-1$ + RenderedParameterInfo offsetParameterInfo = renderingContext.calculateOffsetParameterInfo(); + RenderedParameterInfo fetchFirstParameterInfo = renderingContext.calculateFetchFirstRowsParameterInfo(); + return FragmentAndParameters.withFragment("offset " + offsetParameterInfo.renderedPlaceHolder() //$NON-NLS-1$ + + " rows fetch first " + fetchFirstParameterInfo.renderedPlaceHolder() //$NON-NLS-1$ + " rows only") //$NON-NLS-1$ - .withParameter(parameterInfo1.parameterMapKey(), offset) - .withParameter(parameterInfo2.parameterMapKey(), fetchFirstRows) + .withParameter(offsetParameterInfo.parameterMapKey(), offset) + .withParameter(fetchFirstParameterInfo.parameterMapKey(), fetchFirstRows) .build(); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/LimitAndOffsetPagingModelRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/LimitAndOffsetPagingModelRenderer.java index 008c5a1af..609f4816a 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/render/LimitAndOffsetPagingModelRenderer.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/LimitAndOffsetPagingModelRenderer.java @@ -40,19 +40,19 @@ public FragmentAndParameters render() { } private FragmentAndParameters renderLimitOnly() { - RenderedParameterInfo parameterInfo = renderingContext.calculateParameterInfo(); - return FragmentAndParameters.withFragment("limit " + parameterInfo.renderedPlaceHolder()) //$NON-NLS-1$ - .withParameter(parameterInfo.parameterMapKey(), limit) + RenderedParameterInfo limitParameterInfo = renderingContext.calculateLimitParameterInfo(); + return FragmentAndParameters.withFragment("limit " + limitParameterInfo.renderedPlaceHolder()) //$NON-NLS-1$ + .withParameter(limitParameterInfo.parameterMapKey(), limit) .build(); } private FragmentAndParameters renderLimitAndOffset(Long offset) { - RenderedParameterInfo parameterInfo1 = renderingContext.calculateParameterInfo(); - RenderedParameterInfo parameterInfo2 = renderingContext.calculateParameterInfo(); - return FragmentAndParameters.withFragment("limit " + parameterInfo1.renderedPlaceHolder() //$NON-NLS-1$ - + " offset " + parameterInfo2.renderedPlaceHolder()) //$NON-NLS-1$ - .withParameter(parameterInfo1.parameterMapKey(), limit) - .withParameter(parameterInfo2.parameterMapKey(), offset) + RenderedParameterInfo limitParameterInfo = renderingContext.calculateLimitParameterInfo(); + RenderedParameterInfo offsetParameterInfo = renderingContext.calculateOffsetParameterInfo(); + return FragmentAndParameters.withFragment("limit " + limitParameterInfo.renderedPlaceHolder() //$NON-NLS-1$ + + " offset " + offsetParameterInfo.renderedPlaceHolder()) //$NON-NLS-1$ + .withParameter(limitParameterInfo.parameterMapKey(), limit) + .withParameter(offsetParameterInfo.parameterMapKey(), offset) .build(); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/update/render/UpdateRenderer.java b/src/main/java/org/mybatis/dynamic/sql/update/render/UpdateRenderer.java index f11662c02..352c1de7a 100644 --- a/src/main/java/org/mybatis/dynamic/sql/update/render/UpdateRenderer.java +++ b/src/main/java/org/mybatis/dynamic/sql/update/render/UpdateRenderer.java @@ -109,7 +109,7 @@ private Optional calculateLimitClause() { } private FragmentAndParameters renderLimitClause(Long limit) { - RenderedParameterInfo parameterInfo = renderingContext.calculateParameterInfo(); + RenderedParameterInfo parameterInfo = renderingContext.calculateLimitParameterInfo(); return FragmentAndParameters.withFragment("limit " + parameterInfo.renderedPlaceHolder()) //$NON-NLS-1$ .withParameter(parameterInfo.parameterMapKey(), limit) diff --git a/src/main/java/org/mybatis/dynamic/sql/util/springbatch/SpringBatchCursorReaderSelectModel.java b/src/main/java/org/mybatis/dynamic/sql/util/springbatch/SpringBatchCursorReaderSelectModel.java deleted file mode 100644 index 5486b31de..000000000 --- a/src/main/java/org/mybatis/dynamic/sql/util/springbatch/SpringBatchCursorReaderSelectModel.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2016-2024 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.mybatis.dynamic.sql.util.springbatch; - -import org.mybatis.dynamic.sql.select.SelectModel; -import org.mybatis.dynamic.sql.select.render.SelectStatementProvider; - -public class SpringBatchCursorReaderSelectModel { - - private final SelectModel selectModel; - - public SpringBatchCursorReaderSelectModel(SelectModel selectModel) { - this.selectModel = selectModel; - } - - public SelectStatementProvider render() { - return selectModel.render(SpringBatchUtility.SPRING_BATCH_READER_RENDERING_STRATEGY); - } -} diff --git a/src/main/java/org/mybatis/dynamic/sql/util/springbatch/SpringBatchPagingItemReaderRenderingStrategy.java b/src/main/java/org/mybatis/dynamic/sql/util/springbatch/SpringBatchPagingItemReaderRenderingStrategy.java new file mode 100644 index 000000000..c87c4a03e --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/util/springbatch/SpringBatchPagingItemReaderRenderingStrategy.java @@ -0,0 +1,50 @@ +/* + * Copyright 2016-2024 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.mybatis.dynamic.sql.util.springbatch; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.mybatis.dynamic.sql.render.MyBatis3RenderingStrategy; + +/** + * This rendering strategy should be used for MyBatis3 statements using the + * MyBatisPagingItemReader supplied by mybatis-spring integration + * (http://www.mybatis.org/spring/). + */ +public class SpringBatchPagingItemReaderRenderingStrategy extends MyBatis3RenderingStrategy { + + @Override + public String getFormattedJdbcPlaceholderForPagingParameters(String prefix, String parameterName) { + return "#{" //$NON-NLS-1$ + + parameterName + + "}"; //$NON-NLS-1$ + } + + @Override + public String formatParameterMapKeyForFetchFirstRows(AtomicInteger sequence) { + return "_pagesize"; //$NON-NLS-1$ + } + + @Override + public String formatParameterMapKeyForLimit(AtomicInteger sequence) { + return "_pagesize"; //$NON-NLS-1$ + } + + @Override + public String formatParameterMapKeyForOffset(AtomicInteger sequence) { + return "_skiprows"; //$NON-NLS-1$ + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/util/springbatch/SpringBatchPagingReaderSelectModel.java b/src/main/java/org/mybatis/dynamic/sql/util/springbatch/SpringBatchPagingReaderSelectModel.java deleted file mode 100644 index 648d71218..000000000 --- a/src/main/java/org/mybatis/dynamic/sql/util/springbatch/SpringBatchPagingReaderSelectModel.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2016-2024 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.mybatis.dynamic.sql.util.springbatch; - -import java.util.HashMap; -import java.util.Map; - -import org.mybatis.dynamic.sql.select.SelectModel; -import org.mybatis.dynamic.sql.select.render.SelectStatementProvider; - -public class SpringBatchPagingReaderSelectModel { - - private final SelectModel selectModel; - - public SpringBatchPagingReaderSelectModel(SelectModel selectModel) { - this.selectModel = selectModel; - } - - public SelectStatementProvider render() { - SelectStatementProvider selectStatement = - selectModel.render(SpringBatchUtility.SPRING_BATCH_READER_RENDERING_STRATEGY); - return new LimitAndOffsetDecorator(selectStatement); - } - - public static class LimitAndOffsetDecorator implements SelectStatementProvider { - private final Map parameters = new HashMap<>(); - private final String selectStatement; - - public LimitAndOffsetDecorator(SelectStatementProvider delegate) { - parameters.putAll(delegate.getParameters()); - - selectStatement = delegate.getSelectStatement() - + " LIMIT #{_pagesize} OFFSET #{_skiprows}"; //$NON-NLS-1$ - } - - @Override - public Map getParameters() { - return parameters; - } - - @Override - public String getSelectStatement() { - return selectStatement; - } - } -} diff --git a/src/main/java/org/mybatis/dynamic/sql/util/springbatch/SpringBatchProviderAdapter.java b/src/main/java/org/mybatis/dynamic/sql/util/springbatch/SpringBatchProviderAdapter.java index f212a0d04..ce75697cd 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/springbatch/SpringBatchProviderAdapter.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/springbatch/SpringBatchProviderAdapter.java @@ -17,13 +17,9 @@ import java.util.Map; -import org.mybatis.dynamic.sql.select.render.SelectStatementProvider; - public class SpringBatchProviderAdapter { public String select(Map parameterValues) { - SelectStatementProvider selectStatement = - (SelectStatementProvider) parameterValues.get(SpringBatchUtility.PARAMETER_KEY); - return selectStatement.getSelectStatement(); + return (String) parameterValues.get(SpringBatchUtility.PARAMETER_KEY); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/util/springbatch/SpringBatchReaderRenderingStrategy.java b/src/main/java/org/mybatis/dynamic/sql/util/springbatch/SpringBatchReaderRenderingStrategy.java deleted file mode 100644 index 48b1b9b8c..000000000 --- a/src/main/java/org/mybatis/dynamic/sql/util/springbatch/SpringBatchReaderRenderingStrategy.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2016-2024 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.mybatis.dynamic.sql.util.springbatch; - -import org.mybatis.dynamic.sql.BindableColumn; -import org.mybatis.dynamic.sql.render.MyBatis3RenderingStrategy; - -/** - * This rendering strategy should be used for MyBatis3 statements using one of the - * Spring batch readers supplied by mybatis-spring integration (http://www.mybatis.org/spring/). - * Those readers are MyBatisPagingItemReader and MyBatisCursorItemReader. - * - */ -public class SpringBatchReaderRenderingStrategy extends MyBatis3RenderingStrategy { - - @Override - public String getFormattedJdbcPlaceholder(BindableColumn column, String prefix, String parameterName) { - String newPrefix = SpringBatchUtility.PARAMETER_KEY + "." + prefix; //$NON-NLS-1$ - return super.getFormattedJdbcPlaceholder(column, newPrefix, parameterName); - } -} diff --git a/src/main/java/org/mybatis/dynamic/sql/util/springbatch/SpringBatchUtility.java b/src/main/java/org/mybatis/dynamic/sql/util/springbatch/SpringBatchUtility.java index 63b64429a..01cae8964 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/springbatch/SpringBatchUtility.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/springbatch/SpringBatchUtility.java @@ -18,48 +18,45 @@ import java.util.HashMap; import java.util.Map; -import org.mybatis.dynamic.sql.BasicColumn; import org.mybatis.dynamic.sql.render.RenderingStrategy; -import org.mybatis.dynamic.sql.select.QueryExpressionDSL; -import org.mybatis.dynamic.sql.select.SelectDSL; import org.mybatis.dynamic.sql.select.render.SelectStatementProvider; public class SpringBatchUtility { private SpringBatchUtility() {} - public static final String PARAMETER_KEY = "mybatis3_dsql_query"; //$NON-NLS-1$ - - public static final RenderingStrategy SPRING_BATCH_READER_RENDERING_STRATEGY = - new SpringBatchReaderRenderingStrategy(); - - public static Map toParameterValues(SelectStatementProvider selectStatement) { - Map parameterValues = new HashMap<>(); - parameterValues.put(PARAMETER_KEY, selectStatement); - return parameterValues; - } + static final String PARAMETER_KEY = "mybatis3_dsql_query"; //$NON-NLS-1$ /** - * Select builder that renders in a manner appropriate for the MyBatisPagingItemReader. + * Constant for use in a query intended for use with the MyBatisPagingItemReader. + * This value will not be used in the query at runtime because MyBatis Spring integration + * will supply a value for _skiprows. * - *

Important rendered SQL will contain LIMIT and OFFSET clauses in the SELECT statement. If your database - * (Oracle) does not support LIMIT and OFFSET, the queries will fail. + *

This value can be used as a parameter for the "offset" method in a query to make the intention + * clear that the actual runtime value will be supplied by MyBatis Spring integration. * - * @param selectList a column list for the SELECT statement - * @return FromGatherer used to continue a SELECT statement + *

See https://mybatis.org/spring/batch.html for details. */ - public static QueryExpressionDSL.FromGatherer selectForPaging( - BasicColumn... selectList) { - return SelectDSL.select(SpringBatchPagingReaderSelectModel::new, selectList); - } + public static final long MYBATIS_SPRING_BATCH_SKIPROWS = -437L; /** - * Select builder that renders in a manner appropriate for the MyBatisCursorItemReader. + * Constant for use in a query intended for use with the MyBatisPagingItemReader. + * This value will not be used in the query at runtime because MyBatis Spring integration + * will supply a value for _pagesize. * - * @param selectList a column list for the SELECT statement - * @return FromGatherer used to continue a SELECT statement + *

This value can be used as a parameter for the "limit" or "fetchFirst" method in a query to make the intention + * clear that the actual runtime value will be supplied by MyBatis Spring integration. + * + *

See https://mybatis.org/spring/batch.html for details. */ - public static QueryExpressionDSL.FromGatherer selectForCursor( - BasicColumn... selectList) { - return SelectDSL.select(SpringBatchCursorReaderSelectModel::new, selectList); + public static final long MYBATIS_SPRING_BATCH_PAGESIZE = -439L; + + public static final RenderingStrategy SPRING_BATCH_PAGING_ITEM_READER_RENDERING_STRATEGY = + new SpringBatchPagingItemReaderRenderingStrategy(); + + public static Map toParameterValues(SelectStatementProvider selectStatement) { + var parameterValues = new HashMap(); + parameterValues.put(PARAMETER_KEY, selectStatement.getSelectStatement()); + parameterValues.put(RenderingStrategy.DEFAULT_PARAMETER_PREFIX, selectStatement.getParameters()); + return parameterValues; } } diff --git a/src/site/markdown/docs/springBatch.md b/src/site/markdown/docs/springBatch.md index 4b1855a97..aca011c39 100644 --- a/src/site/markdown/docs/springBatch.md +++ b/src/site/markdown/docs/springBatch.md @@ -1,100 +1,164 @@ # Spring Batch Support This library provides some utilities to make it easier to interact with the MyBatis Spring Batch support. -## The Problem +MyBatis Spring provides support for interacting with Spring Batch (see +[http://www.mybatis.org/spring/batch.html](http://www.mybatis.org/spring/batch.html)). This support consists of +specialized implementations of Spring Batch's `ItemReader` and `ItemWriter` interfaces that have support for MyBatis +mappers. -MyBatis Spring support provides utility classes for interacting with Spring Batch (see [http://www.mybatis.org/spring/batch.html](http://www.mybatis.org/spring/batch.html)). These classes are specialized implementations of Spring Batch's `ItemReader` and `ItemWriter` interfaces that have support for MyBatis mappers. +The `ItemWriter` implementation works with SQL generated by MyBatis Dynamic SQL with no modification needed. -The `ItemWriter` implementations work with SQL generated by MyBatis Dynamic SQL with no modification needed. +The `ItemReader` implementations need special care. Those classes assume that all query parameters will be placed in a +Map (as per usual when using multiple parameters in a query). MyBatis Dynamic SQL, by default, builds a parameter +object that is intended to be the only parameter for a query. The library contains utilities for overcoming this +difficulty. -The `ItemReader` implementations need special care. Those classes assume that all query parameters will be placed in a Map (as per usual when using multiple parameters in a query). MyBatis Dynamic SQL, by default, builds a parameter object that should be the only parameter in a query and will not work when placed in a Map of parameters. +## Using MyBatisCursorItemReader -## The Solution +The `MyBatisCursorItemReader` class works with built-in support for cursor based queries in MyBatis. Queries of this +type will read row by row and MyBatis will convert each result row to a result object without having to read the entire +result set into memory. The normal rendering for MyBatis will work for queries using this reader, but special care +must be taken to prepare the parameter values for use with this reader. See the following example: -The solution involves these steps: - -1. The SQL must be rendered such that the parameter markers are aware of the enclosing parameter Map in the `ItemReader` -1. The `SelectStatementProvider` must be placed in the `ItemReader` parameter Map with a known key. -1. The `@SelectProvider` must be configured to be aware of the enclosing parameter Map +```java +@Bean +public MyBatisCursorItemReader reader(SqlSessionFactory sqlSessionFactory) { + SelectStatementProvider selectStatement = select(person.allColumns()) + .from(person) + .where(lastName, isEqualTo("flintstone")) + .build() + .render(RenderingStrategies.MYBATIS3); + + MyBatisCursorItemReader reader = new MyBatisCursorItemReader<>(); + reader.setQueryId(PersonMapper.class.getName() + ".selectMany"); + reader.setSqlSessionFactory(sqlSessionFactory); + reader.setParameterValues(SpringBatchUtility.toParameterValues(selectStatement)); + return reader; +} +``` -MyBatis Dynamic SQL provides utilities for each of these requirements. Each utility uses a shared Map key for consistency. +Note the use of `SpringBatchUtility.toParameterValues(...)`. This utility will set up the parameter Map correctly for the +rendered statement, and for use with a library supplied `@selectProvider`. See the following for an example of the mapper +method used for the query coded above: -## Spring Batch Item Readers +```java +@Mapper +public interface PersonMapper { + + @SelectProvider(type=SpringBatchProviderAdapter.class, method="select") + @Results({ + @Result(column="id", property="id", id=true), + @Result(column="first_name", property="firstName"), + @Result(column="last_name", property="lastName") + }) + List selectMany(Map parameterValues); +} +``` -MyBatis Spring support supplies two implementations of the `ItemReader` interface: +Note the use of the `SpringBatchProviderAdapter` - that adapter knows how to retrieve the rendered queries from the +parameter map initialed in the method above. -1. `org.mybatis.spring.batch.MyBatisCursorItemReader` - for queries that can be efficiently processed through a single select statement and a cursor -1. `org.mybatis.spring.batch.MyBatisPagingItemReader` - for queries that should be processed as a series of paged selects. Note that MyBatis does not provide any native support for paged queries - it is up to the user to write SQL for paging. The `MyBatisPagingItemWriter` simply makes properties available that specify which page should be read currently. +### Migrating from 1.x Support for MyBatisCursorItemReader -MyBatis Dynamic SQL supplies specialized select statements that will render properly for the different implementations of `ItemReader`: +In version 1.x, the library supplied a special utility for creating a select statement as follows: -1. `SpringBatchUtility.selectForCursor(...)` will create a select statement that is appropriate for the `MyBatisCursorItemReader` - a single select statement that will be read with a cursor -1. `SpringBatchUtility.selectForPaging(...)` will create a select statement that is appropriate for the `MyBatisPagingItemReader` - a select statement that will be called multiple times - one for each page as configured on the batch job. +```java +SelectStatementProvider selectStatement = SpringBatchUtility.selectForCursor(person.allColumns()) + .from(person) + .where(lastName, isEqualTo("flintstone")) + .build() + .render(); +``` -**Very Important:** The paging implementation will only work for databases that support limit and offset in select statements. Fortunately, most databases do support this - with the notable exception of Oracle. +That utility method was limited in capability and has been removed. The new method described above allows the full +capabilities of the library. To migrate, follow these steps: +1. Replace `SpringBatchUtility.selectForCursor(...)` with `SqlBuilder.select(...)` +2. Replace `render()` with `render(RenderingStrategies.MYBATIS3)` -### Rendering for Cursor +## Using MyBatisPagingItemReader -Queries intended for the `MyBatisCursorItemReader` should be rendered as follows: +The `MyBatisPagingItemReader` class works with paging queries - queries that read rows in pages and process page by page +rather than row by row. The normal rendering for MyBatis will work NOT for queries using this reader because MyBatis +Spring support supplies specially named parameters for page size, offset, etc. So the query must be rendered properly +to respond to these parameter values that are supplied at runtime. As with the other reader, special care +must also be taken to prepare the parameter values for use with this reader. See the following example: ```java - SelectStatementProvider selectStatement = SpringBatchUtility.selectForCursor(person.allColumns()) - .from(person) - .where(lastName, isEqualTo("flintstone")) - .build() - .render(); // renders for MyBatisCursorItemReader +@Bean +public MyBatisPagingItemReader reader(SqlSessionFactory sqlSessionFactory) { + SelectStatementProvider selectStatement = select(person.allColumns()) + .from(person) + .where(forPagingTest, isEqualTo(true)) + .orderBy(id) + .limit(SpringBatchUtility.MYBATIS_SPRING_BATCH_PAGESIZE) + .offset(SpringBatchUtility.MYBATIS_SPRING_BATCH_SKIPROWS) + .build() + .render(SpringBatchUtility.SPRING_BATCH_PAGING_ITEM_READER_RENDERING_STRATEGY); + + MyBatisPagingItemReader reader = new MyBatisPagingItemReader<>(); + reader.setQueryId(PersonMapper.class.getName() + ".selectMany"); + reader.setSqlSessionFactory(sqlSessionFactory); + reader.setParameterValues(SpringBatchUtility.toParameterValues(selectStatement)); + reader.setPageSize(7); + return reader; +} ``` - -### Rendering for Paging - -Queries intended for the `MyBatisPagingItemReader` should be rendered as follows: +Notice the following important items: + +1. The `limit` and `offset` methods in the query are used to set up paging support in the query. With MyBatis Spring + batch support, the integration library will supply values for those parameters at runtime. Any values you code in the + select statement will be ignored - only the values supplied by the library will be used. We supply two constants + to make this clearer: `MYBATIS_SPRING_BATCH_PAGESIZE` and `MYBATIS_SPRING_BATCH_SKIPROWS`. You can use these values + to make the code clearer, but again the values will be ignored at runtime. +2. The query must be rendered with the `SPRING_BATCH_PAGING_ITEM_READER_RENDERING_STRATEGY` rendering strategy. This + rendering strategy will render the query so that it will respond properly to the runtime values supplied for page size + and skip rows. +3. Note the use of `SpringBatchUtility.toParameterValues(...)`. This utility will set up the parameter Map correctly for + the rendered statement, and for use with a library supplied `@selectProvider`. See the following for an example of + the mapper method used for the query coded above: ```java - SelectStatementProvider selectStatement = SpringBatchUtility.selectForPaging(person.allColumns()) - .from(person) - .where(lastName, isEqualTo("flintstone")) - .build() - .render(); // renders for MyBatisPagingItemReader +@Mapper +public interface PersonMapper { + + @SelectProvider(type=SpringBatchProviderAdapter.class, method="select") + @Results({ + @Result(column="id", property="id", id=true), + @Result(column="first_name", property="firstName"), + @Result(column="last_name", property="lastName") + }) + List selectMany(Map parameterValues); +} ``` -## Creating the Parameter Map - -The `SpringBatchUtility` provides a method to create the parameter values Map needed by the MyBatis Spring `ItemReader` implementations. It can be used as follows: +Note the use of the `SpringBatchProviderAdapter` - that adapter knows how to retrieve the rendered queries from the +parameter map initialed in the method above. -For cursor based queries... +### Migrating from 1.x Support for MyBatisPagingItemReader -```java - MyBatisCursorItemReader reader = new MyBatisCursorItemReader<>(); - reader.setQueryId(PersonMapper.class.getName() + ".selectMany"); - reader.setSqlSessionFactory(sqlSessionFactory); - reader.setParameterValues(SpringBatchUtility.toParameterValues(selectStatement)); // create parameter map -``` -For paging based queries... +In version 1.x, the library supplied a special utility for creating a select statement as follows: ```java - MyBatisPagingItemReader reader = new MyBatisPagingItemReader<>(); - reader.setQueryId(PersonMapper.class.getName() + ".selectMany"); - reader.setSqlSessionFactory(sqlSessionFactory); - reader.setPageSize(7); - reader.setParameterValues(SpringBatchUtility.toParameterValues(selectStatement)); // create parameter map +SelectStatementProvider selectStatement = SpringBatchUtility.selectForPaging(person.allColumns()) + .from(person) + .where(forPagingTest, isEqualTo(true)) + .orderBy(id) + .build() + .render(); ``` +That utility method was very limited in capability and has been removed. The prior method only supported limit and +offset based queries - which are not supported in all databases. The new method described above allows the full +capabilities of the library to be used. To migrate, follow these steps: -## Specialized @SelectProvider Adapter +1. Replace `SpringBatchUtility.selectForPaging(...)` with `SqlBuilder.select(...)` +2. Add `limit()`, `fetchFirst()`, and `offset()` method calls as appropriate for your query and database +3. Replace `render()` with `render(RenderingStrategies.SPRING_BATCH_PAGING_ITEM_READER_RENDERING_STRATEGY)` -MyBatis mapper methods should be configured to use the specialized `@SelectProvider` adapter as follows: - -```java - @SelectProvider(type=SpringBatchProviderAdapter.class, method="select") // use the Spring batch adapter - @Results({ - @Result(column="id", property="id", id=true), - @Result(column="first_name", property="firstName"), - @Result(column="last_name", property="lastName") - }) - List selectMany(Map parameterValues); -``` -## Complete Example +## Complete Examples -The unit tests for MyBatis Dynamic SQL include a complete example of using MyBatis Spring Batch support using the MyBatis supplied reader as well as both types of MyBatis supplied writers. You can see the full example here: [https://github.com/mybatis/mybatis-dynamic-sql/tree/master/src/test/java/examples/springbatch](https://github.com/mybatis/mybatis-dynamic-sql/tree/master/src/test/java/examples/springbatch) +The unit tests for MyBatis Dynamic SQL include a complete example of using MyBatis Spring Batch support using the +MyBatis supplied reader as well as both types of MyBatis supplied writers. You can see the full example +here: [https://github.com/mybatis/mybatis-dynamic-sql/tree/master/src/test/java/examples/springbatch](https://github.com/mybatis/mybatis-dynamic-sql/tree/master/src/test/java/examples/springbatch) diff --git a/src/test/java/examples/simple/MyBatisMapToRowTest.java b/src/test/java/examples/simple/MyBatisMapToRowTest.java index 2b6f714b2..a542d36f6 100644 --- a/src/test/java/examples/simple/MyBatisMapToRowTest.java +++ b/src/test/java/examples/simple/MyBatisMapToRowTest.java @@ -28,7 +28,6 @@ import java.io.InputStreamReader; import java.sql.Connection; import java.sql.DriverManager; -import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.stream.IntStream; diff --git a/src/test/java/examples/springbatch/SpringBatchRenderingTest.java b/src/test/java/examples/springbatch/SpringBatchRenderingTest.java new file mode 100644 index 000000000..48873b375 --- /dev/null +++ b/src/test/java/examples/springbatch/SpringBatchRenderingTest.java @@ -0,0 +1,65 @@ +/* + * Copyright 2016-2024 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 examples.springbatch; + +import static examples.springbatch.mapper.PersonDynamicSqlSupport.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mybatis.dynamic.sql.SqlBuilder.isLike; +import static org.mybatis.dynamic.sql.SqlBuilder.select; + +import org.junit.jupiter.api.Test; +import org.mybatis.dynamic.sql.util.springbatch.SpringBatchUtility; + +class SpringBatchRenderingTest { + + @Test + void renderLimit() { + var selectStatement = select(person.allColumns()) + .from(person) + .where(firstName, isLike("%f%")) + .limit(SpringBatchUtility.MYBATIS_SPRING_BATCH_PAGESIZE) + .offset(SpringBatchUtility.MYBATIS_SPRING_BATCH_SKIPROWS) + .build() + .render(SpringBatchUtility.SPRING_BATCH_PAGING_ITEM_READER_RENDERING_STRATEGY); + + assertThat(selectStatement.getSelectStatement()) + .isEqualTo(""" + select * \ + from person \ + where first_name like #{parameters.p1,jdbcType=VARCHAR} \ + limit #{_pagesize} \ + offset #{_skiprows}"""); + } + + @Test + void renderFetchFirst() { + var selectStatement = select(person.allColumns()) + .from(person) + .where(firstName, isLike("%f%")) + .offset(SpringBatchUtility.MYBATIS_SPRING_BATCH_SKIPROWS) + .fetchFirst(SpringBatchUtility.MYBATIS_SPRING_BATCH_PAGESIZE).rowsOnly() + .build() + .render(SpringBatchUtility.SPRING_BATCH_PAGING_ITEM_READER_RENDERING_STRATEGY); + + assertThat(selectStatement.getSelectStatement()) + .isEqualTo(""" + select * \ + from person \ + where first_name like #{parameters.p1,jdbcType=VARCHAR} \ + offset #{_skiprows} rows \ + fetch first #{_pagesize} rows only"""); + } +} diff --git a/src/test/java/examples/springbatch/cursor/CursorReaderBatchConfiguration.java b/src/test/java/examples/springbatch/cursor/CursorReaderBatchConfiguration.java index 823f1c631..838fe9d41 100644 --- a/src/test/java/examples/springbatch/cursor/CursorReaderBatchConfiguration.java +++ b/src/test/java/examples/springbatch/cursor/CursorReaderBatchConfiguration.java @@ -18,10 +18,12 @@ import static examples.springbatch.mapper.PersonDynamicSqlSupport.lastName; import static examples.springbatch.mapper.PersonDynamicSqlSupport.person; import static org.mybatis.dynamic.sql.SqlBuilder.isEqualTo; +import static org.mybatis.dynamic.sql.SqlBuilder.select; import javax.sql.DataSource; import org.apache.ibatis.session.SqlSessionFactory; +import org.mybatis.dynamic.sql.render.RenderingStrategies; import org.mybatis.dynamic.sql.select.render.SelectStatementProvider; import org.mybatis.dynamic.sql.update.render.UpdateStatementProvider; import org.mybatis.dynamic.sql.util.springbatch.SpringBatchUtility; @@ -89,11 +91,11 @@ public PlatformTransactionManager transactionManager(DataSource dataSource) { @Bean public MyBatisCursorItemReader reader(SqlSessionFactory sqlSessionFactory) { - SelectStatementProvider selectStatement = SpringBatchUtility.selectForCursor(person.allColumns()) + SelectStatementProvider selectStatement = select(person.allColumns()) .from(person) .where(lastName, isEqualTo("flintstone")) .build() - .render(); + .render(RenderingStrategies.MYBATIS3); MyBatisCursorItemReader reader = new MyBatisCursorItemReader<>(); reader.setQueryId(PersonMapper.class.getName() + ".selectMany"); diff --git a/src/test/java/examples/springbatch/paging/PagingReaderBatchConfiguration.java b/src/test/java/examples/springbatch/paging/PagingReaderBatchConfiguration.java index 41353ab35..2fa74c862 100644 --- a/src/test/java/examples/springbatch/paging/PagingReaderBatchConfiguration.java +++ b/src/test/java/examples/springbatch/paging/PagingReaderBatchConfiguration.java @@ -17,6 +17,7 @@ import static examples.springbatch.mapper.PersonDynamicSqlSupport.*; import static org.mybatis.dynamic.sql.SqlBuilder.isEqualTo; +import static org.mybatis.dynamic.sql.SqlBuilder.select; import javax.sql.DataSource; @@ -88,12 +89,14 @@ public PlatformTransactionManager transactionManager(DataSource dataSource) { @Bean public MyBatisPagingItemReader reader(SqlSessionFactory sqlSessionFactory) { - SelectStatementProvider selectStatement = SpringBatchUtility.selectForPaging(person.allColumns()) + SelectStatementProvider selectStatement = select(person.allColumns()) .from(person) .where(forPagingTest, isEqualTo(true)) .orderBy(id) + .limit(SpringBatchUtility.MYBATIS_SPRING_BATCH_PAGESIZE) + .offset(SpringBatchUtility.MYBATIS_SPRING_BATCH_SKIPROWS) .build() - .render(); + .render(SpringBatchUtility.SPRING_BATCH_PAGING_ITEM_READER_RENDERING_STRATEGY); MyBatisPagingItemReader reader = new MyBatisPagingItemReader<>(); reader.setQueryId(PersonMapper.class.getName() + ".selectMany");