diff --git a/CHANGELOG.md b/CHANGELOG.md index 513bc8d72..48e7cb522 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,18 +8,28 @@ GitHub milestone: [https://github.com/mybatis/mybatis-dynamic-sql/issues?q=miles ### General Announcements -This release includes a significant refactoring of the classes in the "org.mybatis.dynamic.sql.select.function" package. The new classes are more consistent and flexible and should be compatible with existing code at the source level (meaning that code should be recompiled for the new version of the library). +This release includes major improvements to the Spring support in the library. Spring support is now functionally equivalent to MyBatis support. -If you have written your own set of functions to extend the library, you will notice that the base classes 'AbstractFunction" and "AbstractMultipleColumnArithmeticFunction" are now deprecated. Their replacement classes are "AbstractUniTypeFunction" and "OperatorFunction" respectively. +This release includes a significant refactoring of the classes in the "org.mybatis.dynamic.sql.select.function" package. The new classes are more consistent and flexible and should be compatible with existing code at the source level (meaning code should be recompiled for the new version of the library). If you have written your own set of functions to extend the library, you will notice that the base classes 'AbstractFunction" and "AbstractMultipleColumnArithmeticFunction" are now deprecated. Their replacement classes are "AbstractUniTypeFunction" and "OperatorFunction" respectively. + +With this release, we deprecated several insert methods because they were inconsistently named or awkward. All deprecated methods have documented direct replacements. + +In the next major release of the library, all deprecated code will be removed. ### Added - Added a general insert statement that does not require a separate record class to hold values for the insert. ([#201](https://github.com/mybatis/mybatis-dynamic-sql/issues/201)) -- Added the capability to specify a rendering strategy on a column to override the defaut rendering strategy for a statement. This will allow certain edge cases where a parameter marker needs to be formatted in a unique way (for example, "::jsonb" needs to be added to parameter markers for JSON fields in PostgreSQL) ([#200](https://github.com/mybatis/mybatis-dynamic-sql/issues/200)) +- Added the capability to specify a rendering strategy on a column to override the default rendering strategy for a statement. This will allow certain edge cases where a parameter marker needs to be formatted uniquely (for example, "::jsonb" needs to be added to parameter markers for JSON fields in PostgreSQL) ([#200](https://github.com/mybatis/mybatis-dynamic-sql/issues/200)) - Added the ability to write a function that will change the column data type ([#197](https://github.com/mybatis/mybatis-dynamic-sql/issues/197)) - Added the `applyOperator` function to make it easy to use non-standard database operators in expressions ([#220](https://github.com/mybatis/mybatis-dynamic-sql/issues/220)) -- Added convenience methods for count(column) and count(distinct column)([#221](https://github.com/mybatis/mybatis-dynamic-sql/issues/221)) -- Added support for union queries in Kotlin([#187](https://github.com/mybatis/mybatis-dynamic-sql/issues/187)) +- Added convenience methods for count(column) and count(distinct column) ([#221](https://github.com/mybatis/mybatis-dynamic-sql/issues/221)) +- Added support for union queries in Kotlin ([#187](https://github.com/mybatis/mybatis-dynamic-sql/issues/187)) +- Many enhancements for Spring including: + - Fixed a bug where multi-row insert statements did not render properly for Spring ([#224](https://github.com/mybatis/mybatis-dynamic-sql/issues/224)) + - Added support for a parameter type converter for use cases where the Java type of a column does not match the database column type ([#131](https://github.com/mybatis/mybatis-dynamic-sql/issues/131)) + - Added a utility class which simplifies the use of the named parameter JDBC template for Java code - `org.mybatis.dynamic.sql.util.spring.NamedParameterJdbcTemplateExtensions` + - Added support for general inserts, multi-row inserts, batch inserts in the Kotlin DSL for Spring ([#225](https://github.com/mybatis/mybatis-dynamic-sql/issues/225)) + - Added support for generated keys in the Kotlin DSL for Spring ([#226](https://github.com/mybatis/mybatis-dynamic-sql/issues/226)) ## Release 1.1.4 - November 23, 2019 @@ -53,7 +63,7 @@ GitHub milestone: [https://github.com/mybatis/mybatis-dynamic-sql/issues?q=miles ### Added - Changed the public SQLBuilder API to accept Collection instead of List for in conditions and batch record inserts. This should have no impact on existing code, but allow for some future flexibility ([#88](https://github.com/mybatis/mybatis-dynamic-sql/pull/88)) -- Added the ability have have table catalog and/or schema calculated at query runtime. This is useful for situations where there are different database schemas for different environments, or in some sharding situations ([#92](https://github.com/mybatis/mybatis-dynamic-sql/pull/92)) +- Added the ability to have table catalog and/or schema calculated at runtime. This is useful for situations where there are different database schemas for different environments, or in some sharding situations ([#92](https://github.com/mybatis/mybatis-dynamic-sql/pull/92)) - Add support for paging queries with "offset" and "fetch first" - this seems to be standard on most databases ([#96](https://github.com/mybatis/mybatis-dynamic-sql/pull/96)) - Added the ability to call a builder method on any intermediate object in a select statement and receive a fully rendered statement. This makes it easier to build very dynamic queries ([#106](https://github.com/mybatis/mybatis-dynamic-sql/pull/106)) - Add the ability to modify values on any condition before they are placed in the parameter map ([#105](https://github.com/mybatis/mybatis-dynamic-sql/issues/105)) diff --git a/src/main/java/org/mybatis/dynamic/sql/BindableColumn.java b/src/main/java/org/mybatis/dynamic/sql/BindableColumn.java index 7e85f8b9e..6305167b2 100644 --- a/src/main/java/org/mybatis/dynamic/sql/BindableColumn.java +++ b/src/main/java/org/mybatis/dynamic/sql/BindableColumn.java @@ -26,9 +26,7 @@ * * @author Jeff Butler * - * @param - even though the type is not directly used in this class, - * it is used by the compiler to match columns with conditions so it should - * not be removed. + * @param - the Java type that corresponds to this column */ public interface BindableColumn extends BasicColumn { @@ -49,4 +47,8 @@ default Optional typeHandler() { default Optional renderingStrategy() { return Optional.empty(); } + + default Object convertParameterType(T value) { + return value; + } } diff --git a/src/main/java/org/mybatis/dynamic/sql/ParameterTypeConverter.java b/src/main/java/org/mybatis/dynamic/sql/ParameterTypeConverter.java new file mode 100644 index 000000000..58d42b22a --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/ParameterTypeConverter.java @@ -0,0 +1,54 @@ +/** + * Copyright 2016-2020 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mybatis.dynamic.sql; + +/** + * A parameter type converter is used to change a parameter value from one type to another + * during statement rendering and before the parameter is placed into the parameter map. This can be used + * to somewhat mimic the function of a MyBatis type handler for runtimes such as Spring that don't have + * a corresponding concept. + * + *

Since Spring does not have the concept of type handlers, it is a best practice to only use + * Java data types that have a clear correlation to SQL data types (for example Java String correlates + * automatically with VARCHAR). Using a parameter type converter will allow you to use data types in your + * model classes that would otherwise be difficult to use with Spring. + * + *

A parameter type converter is associated with a SqlColumn. + * + *

This interface is based on Spring's general Converter interface and is intentionally compatible with it. + * Existing converters may be reused if they are marked with this additional interface. + * + *

The converter is only used for parameters in a parameter map. It is not used for result set processing. + * It is also not used for insert statements that are based on an external record class. The converter will be called + * in the following circumstances: + * + *

    + *
  • Parameters in a general insert statement (for the Value and ValueWhenPresent mappings)
  • + *
  • Parameters in an update statement (for the Value and ValueWhenPresent mappings)
  • + *
  • Parameters in a where clause in any statement (for conditions that accept a value or multiple values)
  • + *
+ * + * @param Source Type + * @param Target Type + * + * @see SqlColumn + * @author Jeff Butler + * @since 1.1.5 + */ +@FunctionalInterface +public interface ParameterTypeConverter { + T convert(S source); +} diff --git a/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java b/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java index baa1837cf..5c84557c2 100644 --- a/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java +++ b/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java @@ -136,21 +136,84 @@ static DeleteDSL deleteFrom(SqlTable table) { static InsertDSL.IntoGatherer insert(T record) { return InsertDSL.insert(record); } - + + /** + * Insert a Batch of records. The model object is structured to support bulk inserts with JDBC batch support. + * + * @param records records to insert + * @param the type of record to insert + * @return the next step in the DSL + * @deprecated Use {@link SqlBuilder#insertBatch(Object[])} instead + */ + @Deprecated @SafeVarargs static BatchInsertDSL.IntoGatherer insert(T...records) { - return BatchInsertDSL.insert(records); + return insertBatch(records); } - + + /** + * Insert a Batch of records. The model object is structured to support bulk inserts with JDBC batch support. + * + * @param records records to insert + * @param the type of record to insert + * @return the next step in the DSL + * @deprecated Use {@link SqlBuilder#insertBatch(Collection)} instead + */ + @Deprecated static BatchInsertDSL.IntoGatherer insert(Collection records) { + return insertBatch(records); + } + + /** + * Insert a Batch of records. The model object is structured to support bulk inserts with JDBC batch support. + * + * @param records records to insert + * @param the type of record to insert + * @return the next step in the DSL + */ + @SafeVarargs + static BatchInsertDSL.IntoGatherer insertBatch(T...records) { return BatchInsertDSL.insert(records); } - + + /** + * Insert a Batch of records. The model object is structured to support bulk inserts with JDBC batch support. + * + * @param records records to insert + * @param the type of record to insert + * @return the next step in the DSL + */ + static BatchInsertDSL.IntoGatherer insertBatch(Collection records) { + return BatchInsertDSL.insert(records); + } + + /** + * Insert multiple records in a single statement. The model object is structured as a single insert statement with + * multiple values clauses. This statement is suitable for use with a small number of records. It is not suitable + * for large bulk inserts as it is possible to exceed the limit of parameter markers in a prepared statement. + * + *

For large bulk inserts, see {@link SqlBuilder#insertBatch(Object[])} + * + * @param records records to insert + * @param the type of record to insert + * @return the next step in the DSL + */ @SafeVarargs static MultiRowInsertDSL.IntoGatherer insertMultiple(T...records) { return MultiRowInsertDSL.insert(records); } - + + /** + * Insert multiple records in a single statement. The model object is structured as a single insert statement with + * multiple values clauses. This statement is suitable for use with a small number of records. It is not suitable + * for large bulk inserts as it is possible to exceed the limit of parameter markers in a prepared statement. + * + *

For large bulk inserts, see {@link SqlBuilder#insertBatch(Collection)} + * + * @param records records to insert + * @param the type of record to insert + * @return the next step in the DSL + */ static MultiRowInsertDSL.IntoGatherer insertMultiple(Collection records) { return MultiRowInsertDSL.insert(records); } diff --git a/src/main/java/org/mybatis/dynamic/sql/SqlColumn.java b/src/main/java/org/mybatis/dynamic/sql/SqlColumn.java index 47dc81bc3..bff34f5fd 100644 --- a/src/main/java/org/mybatis/dynamic/sql/SqlColumn.java +++ b/src/main/java/org/mybatis/dynamic/sql/SqlColumn.java @@ -19,6 +19,7 @@ import java.util.Objects; import java.util.Optional; +import org.jetbrains.annotations.NotNull; import org.mybatis.dynamic.sql.render.RenderingStrategy; import org.mybatis.dynamic.sql.render.TableAliasCalculator; @@ -31,15 +32,16 @@ public class SqlColumn implements BindableColumn, SortSpecification { protected String alias; protected String typeHandler; protected RenderingStrategy renderingStrategy; - + protected ParameterTypeConverter parameterTypeConverter; + private SqlColumn(Builder builder) { name = Objects.requireNonNull(builder.name); jdbcType = builder.jdbcType; table = Objects.requireNonNull(builder.table); typeHandler = builder.typeHandler; } - - protected SqlColumn(SqlColumn sqlColumn) { + + protected SqlColumn(SqlColumn sqlColumn) { name = sqlColumn.name; table = sqlColumn.table; jdbcType = sqlColumn.jdbcType; @@ -47,6 +49,7 @@ protected SqlColumn(SqlColumn sqlColumn) { alias = sqlColumn.alias; typeHandler = sqlColumn.typeHandler; renderingStrategy = sqlColumn.renderingStrategy; + parameterTypeConverter = sqlColumn.parameterTypeConverter; } public String name() { @@ -72,6 +75,11 @@ public Optional typeHandler() { return Optional.ofNullable(typeHandler); } + @Override + public Object convertParameterType(T value) { + return parameterTypeConverter == null ? value : parameterTypeConverter.convert(value); + } + @Override public SortSpecification descending() { SqlColumn column = new SqlColumn<>(this); @@ -107,19 +115,42 @@ public String renderWithTableAlias(TableAliasCalculator tableAliasCalculator) { public Optional renderingStrategy() { return Optional.ofNullable(renderingStrategy); } - - public SqlColumn withTypeHandler(String typeHandler) { - SqlColumn column = new SqlColumn<>(this); + + @NotNull + public SqlColumn withTypeHandler(String typeHandler) { + SqlColumn column = copy(); column.typeHandler = typeHandler; return column; } + @NotNull public SqlColumn withRenderingStrategy(RenderingStrategy renderingStrategy) { - SqlColumn column = new SqlColumn<>(this); + SqlColumn column = copy(); column.renderingStrategy = renderingStrategy; return column; } + @NotNull + public SqlColumn withParameterTypeConverter(ParameterTypeConverter parameterTypeConverter) { + SqlColumn column = copy(); + column.parameterTypeConverter = parameterTypeConverter; + return column; + } + + /** + * This method helps us tell a bit of fiction to the Java compiler. Java, for better or worse, + * does not carry generic type information through chained methods. We want to enable method + * chaining in the "with" methods. With this bit of fiction, we force the compiler to delay type + * inference to the last method in the chain. + * + * @param the type. Will be the same as T for this usage. + * @return a new SqlColumn of type S (S is the same as T) + */ + @SuppressWarnings("unchecked") + private SqlColumn copy() { + return new SqlColumn<>((SqlColumn) this); + } + private String applyTableAlias(String tableAlias) { return tableAlias + "." + name(); //$NON-NLS-1$ } diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/AbstractMultiRowInsertModel.java b/src/main/java/org/mybatis/dynamic/sql/insert/AbstractMultiRowInsertModel.java index 10168e63b..6fe235a28 100644 --- a/src/main/java/org/mybatis/dynamic/sql/insert/AbstractMultiRowInsertModel.java +++ b/src/main/java/org/mybatis/dynamic/sql/insert/AbstractMultiRowInsertModel.java @@ -1,5 +1,5 @@ /** - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2020 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. diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/BatchInsertDSL.java b/src/main/java/org/mybatis/dynamic/sql/insert/BatchInsertDSL.java index b797fe2ba..75adbeb45 100644 --- a/src/main/java/org/mybatis/dynamic/sql/insert/BatchInsertDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/insert/BatchInsertDSL.java @@ -1,5 +1,5 @@ /** - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2020 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. @@ -23,12 +23,13 @@ import org.mybatis.dynamic.sql.SqlColumn; import org.mybatis.dynamic.sql.SqlTable; import org.mybatis.dynamic.sql.util.AbstractColumnMapping; +import org.mybatis.dynamic.sql.util.Buildable; import org.mybatis.dynamic.sql.util.ConstantMapping; import org.mybatis.dynamic.sql.util.NullMapping; import org.mybatis.dynamic.sql.util.PropertyMapping; import org.mybatis.dynamic.sql.util.StringConstantMapping; -public class BatchInsertDSL { +public class BatchInsertDSL implements Buildable> { private Collection records; private SqlTable table; @@ -43,6 +44,7 @@ public ColumnMappingFinisher map(SqlColumn column) { return new ColumnMappingFinisher<>(column); } + @Override public BatchInsertModel build() { return BatchInsertModel.withRecords(records) .withTable(table) diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/BatchInsertModel.java b/src/main/java/org/mybatis/dynamic/sql/insert/BatchInsertModel.java index d8c259612..052fbeb72 100644 --- a/src/main/java/org/mybatis/dynamic/sql/insert/BatchInsertModel.java +++ b/src/main/java/org/mybatis/dynamic/sql/insert/BatchInsertModel.java @@ -1,5 +1,5 @@ /** - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2020 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. @@ -17,6 +17,7 @@ import java.util.Collection; +import org.jetbrains.annotations.NotNull; import org.mybatis.dynamic.sql.insert.render.BatchInsert; import org.mybatis.dynamic.sql.insert.render.BatchInsertRenderer; import org.mybatis.dynamic.sql.render.RenderingStrategy; @@ -27,6 +28,7 @@ private BatchInsertModel(Builder builder) { super(builder); } + @NotNull public BatchInsert render(RenderingStrategy renderingStrategy) { return BatchInsertRenderer.withBatchInsertModel(this) .withRenderingStrategy(renderingStrategy) diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/render/AbstractMultiRowValuePhraseVisitor.java b/src/main/java/org/mybatis/dynamic/sql/insert/render/AbstractMultiRowValuePhraseVisitor.java new file mode 100644 index 000000000..e4e6bd819 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/insert/render/AbstractMultiRowValuePhraseVisitor.java @@ -0,0 +1,65 @@ +/** + * Copyright 2016-2020 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mybatis.dynamic.sql.insert.render; + +import org.mybatis.dynamic.sql.SqlColumn; +import org.mybatis.dynamic.sql.render.RenderingStrategy; +import org.mybatis.dynamic.sql.util.ConstantMapping; +import org.mybatis.dynamic.sql.util.MultiRowInsertMappingVisitor; +import org.mybatis.dynamic.sql.util.NullMapping; +import org.mybatis.dynamic.sql.util.PropertyMapping; +import org.mybatis.dynamic.sql.util.StringConstantMapping; + +public abstract class AbstractMultiRowValuePhraseVisitor extends MultiRowInsertMappingVisitor { + + protected RenderingStrategy renderingStrategy; + protected String prefix; + + public AbstractMultiRowValuePhraseVisitor(RenderingStrategy renderingStrategy, String prefix) { + this.renderingStrategy = renderingStrategy; + this.prefix = prefix; + } + + @Override + public FieldAndValue visit(NullMapping mapping) { + return FieldAndValue.withFieldName(mapping.columnName()) + .withValuePhrase("null") //$NON-NLS-1$ + .build(); + } + + @Override + public FieldAndValue visit(ConstantMapping mapping) { + return FieldAndValue.withFieldName(mapping.columnName()) + .withValuePhrase(mapping.constant()) + .build(); + } + + @Override + public FieldAndValue visit(StringConstantMapping mapping) { + return FieldAndValue.withFieldName(mapping.columnName()) + .withValuePhrase("'" + mapping.constant() + "'") //$NON-NLS-1$ //$NON-NLS-2$ + .build(); + } + + @Override + public FieldAndValue visit(PropertyMapping mapping) { + return FieldAndValue.withFieldName(mapping.columnName()) + .withValuePhrase(mapping.mapColumn(c -> calculateJdbcPlaceholder(c, mapping.property()))) + .build(); + } + + abstract String calculateJdbcPlaceholder(SqlColumn column, String parameterName); +} diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/render/BatchInsert.java b/src/main/java/org/mybatis/dynamic/sql/insert/render/BatchInsert.java index d3344b999..9ec793279 100644 --- a/src/main/java/org/mybatis/dynamic/sql/insert/render/BatchInsert.java +++ b/src/main/java/org/mybatis/dynamic/sql/insert/render/BatchInsert.java @@ -1,5 +1,5 @@ /** - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2020 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. @@ -56,6 +56,10 @@ public String getInsertStatementSQL() { return insertStatement; } + public List getRecords() { + return Collections.unmodifiableList(records); + } + public static Builder withRecords(List records) { return new Builder().withRecords(records); } diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/render/BatchInsertRenderer.java b/src/main/java/org/mybatis/dynamic/sql/insert/render/BatchInsertRenderer.java index dc4e2a76a..dd29b69bf 100644 --- a/src/main/java/org/mybatis/dynamic/sql/insert/render/BatchInsertRenderer.java +++ b/src/main/java/org/mybatis/dynamic/sql/insert/render/BatchInsertRenderer.java @@ -35,9 +35,9 @@ private BatchInsertRenderer(Builder builder) { } public BatchInsert render() { - MultiRowValuePhraseVisitor visitor = new MultiRowValuePhraseVisitor(renderingStrategy, "record"); //$NON-NLS-1$) + BatchValuePhraseVisitor visitor = new BatchValuePhraseVisitor(renderingStrategy, "record"); //$NON-NLS-1$) List fieldsAndValues = model - .mapColumnMappings(MultiRowRenderingUtilities.toFieldAndValue(visitor)) + .mapColumnMappings(m -> m.accept(visitor)) .collect(Collectors.toList()); return BatchInsert.withRecords(model.records()) @@ -48,11 +48,17 @@ public BatchInsert render() { private String calculateInsertStatement(List fieldsAndValues) { return "insert into" //$NON-NLS-1$ + spaceBefore(model.table().tableNameAtRuntime()) - + spaceBefore(MultiRowRenderingUtilities.calculateColumnsPhrase(fieldsAndValues)) - + spaceBefore(calculateVluesPhrase(fieldsAndValues)); + + spaceBefore(calculateColumnsPhrase(fieldsAndValues)) + + spaceBefore(calculateValuesPhrase(fieldsAndValues)); } - private String calculateVluesPhrase(List fieldsAndValues) { + private String calculateColumnsPhrase(List fieldsAndValues) { + return fieldsAndValues.stream() + .map(FieldAndValue::fieldName) + .collect(Collectors.joining(", ", "(", ")")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } + + private String calculateValuesPhrase(List fieldsAndValues) { return fieldsAndValues.stream() .map(FieldAndValue::valuePhrase) .collect(Collectors.joining(", ", "values (", ")")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/render/BatchValuePhraseVisitor.java b/src/main/java/org/mybatis/dynamic/sql/insert/render/BatchValuePhraseVisitor.java new file mode 100644 index 000000000..8c9d7f4a3 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/insert/render/BatchValuePhraseVisitor.java @@ -0,0 +1,32 @@ +/** + * Copyright 2016-2020 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mybatis.dynamic.sql.insert.render; + +import org.mybatis.dynamic.sql.SqlColumn; +import org.mybatis.dynamic.sql.render.RenderingStrategy; + +public class BatchValuePhraseVisitor extends AbstractMultiRowValuePhraseVisitor { + + public BatchValuePhraseVisitor(RenderingStrategy renderingStrategy, String prefix) { + super(renderingStrategy, prefix); + } + + @Override + String calculateJdbcPlaceholder(SqlColumn column, String parameterName) { + return column.renderingStrategy().orElse(renderingStrategy) + .getFormattedJdbcPlaceholder(column, prefix, parameterName); + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/render/FieldAndValue.java b/src/main/java/org/mybatis/dynamic/sql/insert/render/FieldAndValue.java index 1158b4ac2..7df02497a 100644 --- a/src/main/java/org/mybatis/dynamic/sql/insert/render/FieldAndValue.java +++ b/src/main/java/org/mybatis/dynamic/sql/insert/render/FieldAndValue.java @@ -35,10 +35,6 @@ public String valuePhrase() { return valuePhrase; } - public String valuePhrase(int row) { - return String.format(valuePhrase, row); - } - public static Builder withFieldName(String fieldName) { return new Builder().withFieldName(fieldName); } diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/render/GeneralInsertRenderer.java b/src/main/java/org/mybatis/dynamic/sql/insert/render/GeneralInsertRenderer.java index 2eb1d91e1..dc219f243 100644 --- a/src/main/java/org/mybatis/dynamic/sql/insert/render/GeneralInsertRenderer.java +++ b/src/main/java/org/mybatis/dynamic/sql/insert/render/GeneralInsertRenderer.java @@ -22,12 +22,10 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.function.Function; import java.util.stream.Collectors; import org.mybatis.dynamic.sql.insert.GeneralInsertModel; import org.mybatis.dynamic.sql.render.RenderingStrategy; -import org.mybatis.dynamic.sql.util.AbstractColumnMapping; public class GeneralInsertRenderer { @@ -41,7 +39,7 @@ private GeneralInsertRenderer(Builder builder) { public GeneralInsertStatementProvider render() { GeneralInsertValuePhraseVisitor visitor = new GeneralInsertValuePhraseVisitor(renderingStrategy); - List> fieldsAndValues = model.mapColumnMappings(toFieldAndValue(visitor)) + List> fieldsAndValues = model.mapColumnMappings(m -> m.accept(visitor)) .collect(Collectors.toList()); return DefaultGeneralInsertStatementProvider.withInsertStatement(calculateInsertStatement(fieldsAndValues)) @@ -56,16 +54,6 @@ private String calculateInsertStatement(List> toFieldAndValue( - GeneralInsertValuePhraseVisitor visitor) { - return insertMapping -> toFieldAndValue(visitor, insertMapping); - } - - private Optional toFieldAndValue(GeneralInsertValuePhraseVisitor visitor, - AbstractColumnMapping insertMapping) { - return insertMapping.accept(visitor); - } - private String calculateColumnsPhrase(List> fieldsAndValues) { return fieldsAndValues.stream() .filter(Optional::isPresent) diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/render/GeneralInsertValuePhraseVisitor.java b/src/main/java/org/mybatis/dynamic/sql/insert/render/GeneralInsertValuePhraseVisitor.java index da6c9ba79..5a8516a6f 100644 --- a/src/main/java/org/mybatis/dynamic/sql/insert/render/GeneralInsertValuePhraseVisitor.java +++ b/src/main/java/org/mybatis/dynamic/sql/insert/render/GeneralInsertValuePhraseVisitor.java @@ -17,7 +17,6 @@ import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Function; import org.mybatis.dynamic.sql.SqlColumn; import org.mybatis.dynamic.sql.render.RenderingStrategy; @@ -40,48 +39,53 @@ public GeneralInsertValuePhraseVisitor(RenderingStrategy renderingStrategy) { @Override public Optional visit(NullMapping mapping) { - return FieldAndValueAndParameters.withFieldName(mapping.mapColumn(SqlColumn::name)) + return FieldAndValueAndParameters.withFieldName(mapping.columnName()) .withValuePhrase("null") //$NON-NLS-1$ .buildOptional(); } @Override public Optional visit(ConstantMapping mapping) { - return FieldAndValueAndParameters.withFieldName(mapping.mapColumn(SqlColumn::name)) + return FieldAndValueAndParameters.withFieldName(mapping.columnName()) .withValuePhrase(mapping.constant()) .buildOptional(); } @Override public Optional visit(StringConstantMapping mapping) { - return FieldAndValueAndParameters.withFieldName(mapping.mapColumn(SqlColumn::name)) + return FieldAndValueAndParameters.withFieldName(mapping.columnName()) .withValuePhrase("'" + mapping.constant() + "'") //$NON-NLS-1$ //$NON-NLS-2$ .buildOptional(); } @Override public Optional visit(ValueMapping mapping) { - return buildFragment(mapping, mapping.value()); + return buildValueFragment(mapping, mapping.value()); } @Override public Optional visit(ValueWhenPresentMapping mapping) { - return mapping.value().flatMap(v -> buildFragment(mapping, v)); + return mapping.value().flatMap(v -> buildValueFragment(mapping, v)); } - - private Optional buildFragment(AbstractColumnMapping mapping, T value) { + + private Optional buildValueFragment(AbstractColumnMapping mapping, + Object value) { + return buildFragment(mapping, value); + } + + private Optional buildFragment(AbstractColumnMapping mapping, Object value) { String mapKey = RenderingStrategy.formatParameterMapKey(sequence); - String jdbcPlaceholder = mapping.mapColumn(toJdbcPlaceholder(mapKey)); + String jdbcPlaceholder = mapping.mapColumn(c -> calculateJdbcPlaceholder(c, mapKey)); - return FieldAndValueAndParameters.withFieldName(mapping.mapColumn(SqlColumn::name)) + return FieldAndValueAndParameters.withFieldName(mapping.columnName()) .withValuePhrase(jdbcPlaceholder) .withParameter(mapKey, value) .buildOptional(); } - private Function, String> toJdbcPlaceholder(String parameterName) { - return column -> column.renderingStrategy().orElse(renderingStrategy) + private String calculateJdbcPlaceholder(SqlColumn column, String parameterName) { + return column.renderingStrategy().orElse(renderingStrategy) .getFormattedJdbcPlaceholder(column, RenderingStrategy.DEFAULT_PARAMETER_PREFIX, parameterName); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/render/InsertRenderer.java b/src/main/java/org/mybatis/dynamic/sql/insert/render/InsertRenderer.java index 7709612a0..795bdf759 100644 --- a/src/main/java/org/mybatis/dynamic/sql/insert/render/InsertRenderer.java +++ b/src/main/java/org/mybatis/dynamic/sql/insert/render/InsertRenderer.java @@ -20,12 +20,10 @@ import java.util.List; import java.util.Objects; import java.util.Optional; -import java.util.function.Function; import java.util.stream.Collectors; import org.mybatis.dynamic.sql.insert.InsertModel; import org.mybatis.dynamic.sql.render.RenderingStrategy; -import org.mybatis.dynamic.sql.util.AbstractColumnMapping; public class InsertRenderer { @@ -40,7 +38,7 @@ private InsertRenderer(Builder builder) { public InsertStatementProvider render() { ValuePhraseVisitor visitor = new ValuePhraseVisitor(renderingStrategy); - List> fieldsAndValues = model.mapColumnMappings(toFieldAndValue(visitor)) + List> fieldsAndValues = model.mapColumnMappings(m -> m.accept(visitor)) .collect(Collectors.toList()); return DefaultInsertStatementProvider.withRecord(model.record()) @@ -71,14 +69,6 @@ private String calculateValuesPhrase(List> fieldsAndValu .collect(Collectors.joining(", ", "values (", ")")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } - private Function> toFieldAndValue(ValuePhraseVisitor visitor) { - return insertMapping -> toFieldAndValue(visitor, insertMapping); - } - - private Optional toFieldAndValue(ValuePhraseVisitor visitor, AbstractColumnMapping insertMapping) { - return insertMapping.accept(visitor); - } - public static Builder withInsertModel(InsertModel model) { return new Builder().withInsertModel(model); } diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/render/MultiRowInsertRenderer.java b/src/main/java/org/mybatis/dynamic/sql/insert/render/MultiRowInsertRenderer.java index c3e8d3058..3cdc487b2 100644 --- a/src/main/java/org/mybatis/dynamic/sql/insert/render/MultiRowInsertRenderer.java +++ b/src/main/java/org/mybatis/dynamic/sql/insert/render/MultiRowInsertRenderer.java @@ -36,10 +36,11 @@ private MultiRowInsertRenderer(Builder builder) { } public MultiRowInsertStatementProvider render() { + // the prefix is a generic format that will be resolved below with String.format(...) MultiRowValuePhraseVisitor visitor = new MultiRowValuePhraseVisitor(renderingStrategy, "records[%s]"); //$NON-NLS-1$ List fieldsAndValues = model - .mapColumnMappings(MultiRowRenderingUtilities.toFieldAndValue(visitor)) + .mapColumnMappings(m -> m.accept(visitor)) .collect(Collectors.toList()); return new DefaultMultiRowInsertStatementProvider.Builder().withRecords(model.records()) @@ -50,10 +51,16 @@ public MultiRowInsertStatementProvider render() { private String calculateInsertStatement(List fieldsAndValues) { return "insert into" //$NON-NLS-1$ + spaceBefore(model.table().tableNameAtRuntime()) - + spaceBefore(MultiRowRenderingUtilities.calculateColumnsPhrase(fieldsAndValues)) + + spaceBefore(calculateColumnsPhrase(fieldsAndValues)) + spaceBefore(calculateMultiRowInsertValuesPhrase(fieldsAndValues, model.recordCount())); } + private String calculateColumnsPhrase(List fieldsAndValues) { + return fieldsAndValues.stream() + .map(FieldAndValue::fieldName) + .collect(Collectors.joining(", ", "(", ")")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } + private String calculateMultiRowInsertValuesPhrase(List fieldsAndValues, int rowCount) { return IntStream.range(0, rowCount) .mapToObj(i -> toSingleRowOfValues(fieldsAndValues, i)) @@ -62,7 +69,8 @@ private String calculateMultiRowInsertValuesPhrase(List fieldsAnd private String toSingleRowOfValues(List fieldsAndValues, int row) { return fieldsAndValues.stream() - .map(fmv -> fmv.valuePhrase(row)) + .map(FieldAndValue::valuePhrase) + .map(s -> String.format(s, row)) .collect(Collectors.joining(", ", "(", ")")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/render/MultiRowRenderingUtilities.java b/src/main/java/org/mybatis/dynamic/sql/insert/render/MultiRowRenderingUtilities.java deleted file mode 100644 index 1ebf1f192..000000000 --- a/src/main/java/org/mybatis/dynamic/sql/insert/render/MultiRowRenderingUtilities.java +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Copyright 2016-2020 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 - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.mybatis.dynamic.sql.insert.render; - -import java.util.List; -import java.util.function.Function; -import java.util.stream.Collectors; - -import org.mybatis.dynamic.sql.util.AbstractColumnMapping; - -public class MultiRowRenderingUtilities { - - private MultiRowRenderingUtilities() {} - - public static Function toFieldAndValue(MultiRowValuePhraseVisitor visitor) { - return insertMapping -> MultiRowRenderingUtilities.toFieldAndValue(visitor, insertMapping); - } - - public static FieldAndValue toFieldAndValue(MultiRowValuePhraseVisitor visitor, - AbstractColumnMapping insertMapping) { - return insertMapping.accept(visitor); - } - - public static String calculateColumnsPhrase(List fieldsAndValues) { - return fieldsAndValues.stream() - .map(FieldAndValue::fieldName) - .collect(Collectors.joining(", ", "(", ")")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ - } - -} diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/render/MultiRowValuePhraseVisitor.java b/src/main/java/org/mybatis/dynamic/sql/insert/render/MultiRowValuePhraseVisitor.java index b5cc87441..b6da8f42c 100644 --- a/src/main/java/org/mybatis/dynamic/sql/insert/render/MultiRowValuePhraseVisitor.java +++ b/src/main/java/org/mybatis/dynamic/sql/insert/render/MultiRowValuePhraseVisitor.java @@ -15,56 +15,18 @@ */ package org.mybatis.dynamic.sql.insert.render; -import java.util.function.Function; - import org.mybatis.dynamic.sql.SqlColumn; import org.mybatis.dynamic.sql.render.RenderingStrategy; -import org.mybatis.dynamic.sql.util.ConstantMapping; -import org.mybatis.dynamic.sql.util.MultiRowInsertMappingVisitor; -import org.mybatis.dynamic.sql.util.NullMapping; -import org.mybatis.dynamic.sql.util.PropertyMapping; -import org.mybatis.dynamic.sql.util.StringConstantMapping; - -public class MultiRowValuePhraseVisitor extends MultiRowInsertMappingVisitor { - private RenderingStrategy renderingStrategy; - private String prefix; +public class MultiRowValuePhraseVisitor extends AbstractMultiRowValuePhraseVisitor { public MultiRowValuePhraseVisitor(RenderingStrategy renderingStrategy, String prefix) { - this.renderingStrategy = renderingStrategy; - this.prefix = prefix; - } - - @Override - public FieldAndValue visit(NullMapping mapping) { - return FieldAndValue.withFieldName(mapping.mapColumn(SqlColumn::name)) - .withValuePhrase("null") //$NON-NLS-1$ - .build(); - } - - @Override - public FieldAndValue visit(ConstantMapping mapping) { - return FieldAndValue.withFieldName(mapping.mapColumn(SqlColumn::name)) - .withValuePhrase(mapping.constant()) - .build(); + super(renderingStrategy, prefix); } @Override - public FieldAndValue visit(StringConstantMapping mapping) { - return FieldAndValue.withFieldName(mapping.mapColumn(SqlColumn::name)) - .withValuePhrase("'" + mapping.constant() + "'") //$NON-NLS-1$ //$NON-NLS-2$ - .build(); - } - - @Override - public FieldAndValue visit(PropertyMapping mapping) { - return FieldAndValue.withFieldName(mapping.mapColumn(SqlColumn::name)) - .withValuePhrase(mapping.mapColumn(toJdbcPlaceholder(mapping.property()))) - .build(); - } - - private Function, String> toJdbcPlaceholder(String parameterName) { - return column -> column.renderingStrategy().orElse(renderingStrategy) - .getFormattedJdbcPlaceholder(column, prefix, parameterName); + String calculateJdbcPlaceholder(SqlColumn column, String parameterName) { + return column.renderingStrategy().orElse(renderingStrategy) + .getMultiRowFormattedJdbcPlaceholder(column, prefix, parameterName); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/render/ValuePhraseVisitor.java b/src/main/java/org/mybatis/dynamic/sql/insert/render/ValuePhraseVisitor.java index 3f7f1043b..3b040e12a 100644 --- a/src/main/java/org/mybatis/dynamic/sql/insert/render/ValuePhraseVisitor.java +++ b/src/main/java/org/mybatis/dynamic/sql/insert/render/ValuePhraseVisitor.java @@ -16,7 +16,6 @@ package org.mybatis.dynamic.sql.insert.render; import java.util.Optional; -import java.util.function.Function; import org.mybatis.dynamic.sql.SqlColumn; import org.mybatis.dynamic.sql.render.RenderingStrategy; @@ -37,29 +36,29 @@ public ValuePhraseVisitor(RenderingStrategy renderingStrategy) { @Override public Optional visit(NullMapping mapping) { - return FieldAndValue.withFieldName(mapping.mapColumn(SqlColumn::name)) + return FieldAndValue.withFieldName(mapping.columnName()) .withValuePhrase("null") //$NON-NLS-1$ .buildOptional(); } @Override public Optional visit(ConstantMapping mapping) { - return FieldAndValue.withFieldName(mapping.mapColumn(SqlColumn::name)) + return FieldAndValue.withFieldName(mapping.columnName()) .withValuePhrase(mapping.constant()) .buildOptional(); } @Override public Optional visit(StringConstantMapping mapping) { - return FieldAndValue.withFieldName(mapping.mapColumn(SqlColumn::name)) + return FieldAndValue.withFieldName(mapping.columnName()) .withValuePhrase("'" + mapping.constant() + "'") //$NON-NLS-1$ //$NON-NLS-2$ .buildOptional(); } @Override public Optional visit(PropertyMapping mapping) { - return FieldAndValue.withFieldName(mapping.mapColumn(SqlColumn::name)) - .withValuePhrase(mapping.mapColumn(toJdbcPlaceholder(mapping.property()))) + return FieldAndValue.withFieldName(mapping.columnName()) + .withValuePhrase(mapping.mapColumn(c -> calculateJdbcPlaceholder(c, mapping.property()))) .buildOptional(); } @@ -72,8 +71,8 @@ public Optional visit(PropertyWhenPresentMapping mapping) { } } - private Function, String> toJdbcPlaceholder(String parameterName) { - return column -> column.renderingStrategy().orElse(renderingStrategy) + private String calculateJdbcPlaceholder(SqlColumn column, String parameterName) { + return column.renderingStrategy().orElse(renderingStrategy) .getFormattedJdbcPlaceholder(column, "record", parameterName); //$NON-NLS-1$ } } 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 9b76a4413..5f69e682a 100644 --- a/src/main/java/org/mybatis/dynamic/sql/render/RenderingStrategy.java +++ b/src/main/java/org/mybatis/dynamic/sql/render/RenderingStrategy.java @@ -1,5 +1,5 @@ /** - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2020 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. @@ -47,4 +47,8 @@ public static String formatParameterMapKey(AtomicInteger sequence) { public abstract String getFormattedJdbcPlaceholder(BindableColumn column, String prefix, String parameterName); public abstract String getFormattedJdbcPlaceholder(String prefix, String parameterName); + + public String getMultiRowFormattedJdbcPlaceholder(BindableColumn column, String prefix, String parameterName) { + return getFormattedJdbcPlaceholder(column, prefix, parameterName); + } } diff --git a/src/main/java/org/mybatis/dynamic/sql/render/SpringNamedParameterRenderingStrategy.java b/src/main/java/org/mybatis/dynamic/sql/render/SpringNamedParameterRenderingStrategy.java index 9037d4f46..7dd48ab74 100644 --- a/src/main/java/org/mybatis/dynamic/sql/render/SpringNamedParameterRenderingStrategy.java +++ b/src/main/java/org/mybatis/dynamic/sql/render/SpringNamedParameterRenderingStrategy.java @@ -1,5 +1,5 @@ /** - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2020 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. @@ -28,4 +28,9 @@ public String getFormattedJdbcPlaceholder(BindableColumn column, String prefi public String getFormattedJdbcPlaceholder(String prefix, String parameterName) { return ":" + parameterName; //$NON-NLS-1$ } + + @Override + public String getMultiRowFormattedJdbcPlaceholder(BindableColumn column, String prefix, String parameterName) { + return ":" + prefix + "." + parameterName; //$NON-NLS-1$ //$NON-NLS-2$ + } } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/MyBatis3SelectModelAdapter.java b/src/main/java/org/mybatis/dynamic/sql/select/MyBatis3SelectModelAdapter.java index baf72eadb..8d009033a 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/MyBatis3SelectModelAdapter.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/MyBatis3SelectModelAdapter.java @@ -1,5 +1,5 @@ /** - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2020 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. @@ -24,7 +24,7 @@ /** * This adapter will render the underlying select model for MyBatis3, and then call a MyBatis mapper method. * - * @deprecated in favor is {@link SelectDSLCompleter}. This class will be removed without direct replacement + * @deprecated in favor of {@link SelectDSLCompleter}. This class will be removed without direct replacement * in a future version * * @author Jeff Butler diff --git a/src/main/java/org/mybatis/dynamic/sql/update/UpdateModel.java b/src/main/java/org/mybatis/dynamic/sql/update/UpdateModel.java index f4f5e7124..b0f9b2fa7 100644 --- a/src/main/java/org/mybatis/dynamic/sql/update/UpdateModel.java +++ b/src/main/java/org/mybatis/dynamic/sql/update/UpdateModel.java @@ -1,5 +1,5 @@ /** - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2020 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. diff --git a/src/main/java/org/mybatis/dynamic/sql/update/render/SetPhraseVisitor.java b/src/main/java/org/mybatis/dynamic/sql/update/render/SetPhraseVisitor.java index 53861df40..77a230af0 100644 --- a/src/main/java/org/mybatis/dynamic/sql/update/render/SetPhraseVisitor.java +++ b/src/main/java/org/mybatis/dynamic/sql/update/render/SetPhraseVisitor.java @@ -18,7 +18,6 @@ import java.util.Objects; import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Function; import org.mybatis.dynamic.sql.SqlColumn; import org.mybatis.dynamic.sql.render.RenderingStrategy; @@ -48,20 +47,20 @@ public SetPhraseVisitor(AtomicInteger sequence, RenderingStrategy renderingStrat @Override public Optional visit(NullMapping mapping) { - return FragmentAndParameters.withFragment(mapping.mapColumn(SqlColumn::name) + " = null") //$NON-NLS-1$ + return FragmentAndParameters.withFragment(mapping.columnName() + " = null") //$NON-NLS-1$ .buildOptional(); } @Override public Optional visit(ConstantMapping mapping) { - String fragment = mapping.mapColumn(SqlColumn::name) + " = " + mapping.constant(); //$NON-NLS-1$ + String fragment = mapping.columnName() + " = " + mapping.constant(); //$NON-NLS-1$ return FragmentAndParameters.withFragment(fragment) .buildOptional(); } @Override public Optional visit(StringConstantMapping mapping) { - String fragment = mapping.mapColumn(SqlColumn::name) + String fragment = mapping.columnName() + " = '" //$NON-NLS-1$ + mapping.constant() + "'"; //$NON-NLS-1$ @@ -88,7 +87,7 @@ public Optional visit(SelectMapping mapping) { .build() .render(); - String fragment = mapping.mapColumn(SqlColumn::name) + String fragment = mapping.columnName() + " = (" //$NON-NLS-1$ + selectStatement.getSelectStatement() + ")"; //$NON-NLS-1$ @@ -100,7 +99,7 @@ public Optional visit(SelectMapping mapping) { @Override public Optional visit(ColumnToColumnMapping mapping) { - String setPhrase = mapping.mapColumn(SqlColumn::name) + String setPhrase = mapping.columnName() + " = " //$NON-NLS-1$ + mapping.rightColumn().renderWithTableAlias(TableAliasCalculator.empty()); @@ -111,8 +110,8 @@ public Optional visit(ColumnToColumnMapping mapping) { private Optional buildFragment(AbstractColumnMapping mapping, T value) { String mapKey = RenderingStrategy.formatParameterMapKey(sequence); - String jdbcPlaceholder = mapping.mapColumn(toJdbcPlaceholder(mapKey)); - String setPhrase = mapping.mapColumn(SqlColumn::name) + String jdbcPlaceholder = mapping.mapColumn(c -> calculateJdbcPlaceholder(c, mapKey)); + String setPhrase = mapping.columnName() + " = " //$NON-NLS-1$ + jdbcPlaceholder; @@ -121,8 +120,8 @@ private Optional buildFragment(AbstractColumnMapping .buildOptional(); } - private Function, String> toJdbcPlaceholder(String parameterName) { - return column -> column.renderingStrategy().orElse(renderingStrategy) + private String calculateJdbcPlaceholder(SqlColumn column, String parameterName) { + return column.renderingStrategy().orElse(renderingStrategy) .getFormattedJdbcPlaceholder(column, RenderingStrategy.DEFAULT_PARAMETER_PREFIX, parameterName); } } 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 575ff1451..cfa2465b7 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 @@ -23,13 +23,11 @@ import java.util.Objects; import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Function; import java.util.stream.Collectors; import org.mybatis.dynamic.sql.render.RenderingStrategy; import org.mybatis.dynamic.sql.render.TableAliasCalculator; import org.mybatis.dynamic.sql.update.UpdateModel; -import org.mybatis.dynamic.sql.util.AbstractColumnMapping; import org.mybatis.dynamic.sql.util.FragmentAndParameters; import org.mybatis.dynamic.sql.where.WhereModel; import org.mybatis.dynamic.sql.where.render.WhereClauseProvider; @@ -49,7 +47,7 @@ public UpdateStatementProvider render() { SetPhraseVisitor visitor = new SetPhraseVisitor(sequence, renderingStrategy); List> fragmentsAndParameters = - updateModel.mapColumnMappings(toFragmentAndParameters(visitor)) + updateModel.mapColumnMappings(m -> m.accept(visitor)) .collect(Collectors.toList()); return updateModel.whereModel() @@ -111,16 +109,6 @@ private Optional renderWhereClause(WhereModel whereModel) { .render(); } - private Function> toFragmentAndParameters( - SetPhraseVisitor visitor) { - return updateMapping -> toFragmentAndParameters(visitor, updateMapping); - } - - private Optional toFragmentAndParameters(SetPhraseVisitor visitor, - AbstractColumnMapping updateMapping) { - return updateMapping.accept(visitor); - } - public static Builder withUpdateModel(UpdateModel updateModel) { return new Builder().withUpdateModel(updateModel); } diff --git a/src/main/java/org/mybatis/dynamic/sql/util/AbstractColumnMapping.java b/src/main/java/org/mybatis/dynamic/sql/util/AbstractColumnMapping.java index ec91c292a..0299b4d6a 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/AbstractColumnMapping.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/AbstractColumnMapping.java @@ -27,6 +27,10 @@ protected AbstractColumnMapping(SqlColumn column) { this.column = Objects.requireNonNull(column); } + public String columnName() { + return column.name(); + } + public R mapColumn(Function, R> mapper) { return mapper.apply(column); } diff --git a/src/main/java/org/mybatis/dynamic/sql/util/Buildable.java b/src/main/java/org/mybatis/dynamic/sql/util/Buildable.java index 2d91d46e7..b0feba673 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/Buildable.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/Buildable.java @@ -1,5 +1,5 @@ /** - * Copyright 2016-2017 the original author or authors. + * Copyright 2016-2020 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. @@ -15,7 +15,10 @@ */ package org.mybatis.dynamic.sql.util; +import org.jetbrains.annotations.NotNull; + @FunctionalInterface public interface Buildable { + @NotNull T build(); } \ No newline at end of file diff --git a/src/main/java/org/mybatis/dynamic/sql/util/ColumnMappingVisitor.java b/src/main/java/org/mybatis/dynamic/sql/util/ColumnMappingVisitor.java index 1d66a780d..559d54d30 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/ColumnMappingVisitor.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/ColumnMappingVisitor.java @@ -27,24 +27,24 @@ * * @author Jeff Butler * - * @param The type of object created by the visitor + * @param The type of object created by the visitor */ -public interface ColumnMappingVisitor { - T visit(NullMapping mapping); +public interface ColumnMappingVisitor { + R visit(NullMapping mapping); - T visit(ConstantMapping mapping); + R visit(ConstantMapping mapping); - T visit(StringConstantMapping mapping); + R visit(StringConstantMapping mapping); - T visit(ValueMapping mapping); + R visit(ValueMapping mapping); - T visit(ValueWhenPresentMapping mapping); + R visit(ValueWhenPresentMapping mapping); - T visit(SelectMapping mapping); + R visit(SelectMapping mapping); - T visit(PropertyMapping mapping); + R visit(PropertyMapping mapping); - T visit(PropertyWhenPresentMapping mapping); + R visit(PropertyWhenPresentMapping mapping); - T visit(ColumnToColumnMapping columnMapping); + R visit(ColumnToColumnMapping columnMapping); } diff --git a/src/main/java/org/mybatis/dynamic/sql/util/ColumnToColumnMapping.java b/src/main/java/org/mybatis/dynamic/sql/util/ColumnToColumnMapping.java index c7649a446..2e43cc745 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/ColumnToColumnMapping.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/ColumnToColumnMapping.java @@ -1,5 +1,5 @@ /** - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2020 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. diff --git a/src/main/java/org/mybatis/dynamic/sql/util/ConstantMapping.java b/src/main/java/org/mybatis/dynamic/sql/util/ConstantMapping.java index f2942454c..1cdef2fe9 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/ConstantMapping.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/ConstantMapping.java @@ -1,5 +1,5 @@ /** - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2020 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. diff --git a/src/main/java/org/mybatis/dynamic/sql/util/GeneralInsertMappingVisitor.java b/src/main/java/org/mybatis/dynamic/sql/util/GeneralInsertMappingVisitor.java index 60e86c1e9..eb689961d 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/GeneralInsertMappingVisitor.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/GeneralInsertMappingVisitor.java @@ -15,24 +15,24 @@ */ package org.mybatis.dynamic.sql.util; -public abstract class GeneralInsertMappingVisitor implements ColumnMappingVisitor { +public abstract class GeneralInsertMappingVisitor implements ColumnMappingVisitor { @Override - public final T visit(SelectMapping mapping) { + public final R visit(SelectMapping mapping) { throw new UnsupportedOperationException(); } @Override - public final T visit(PropertyMapping mapping) { + public final R visit(PropertyMapping mapping) { throw new UnsupportedOperationException(); } @Override - public final T visit(PropertyWhenPresentMapping mapping) { + public final R visit(PropertyWhenPresentMapping mapping) { throw new UnsupportedOperationException(); } @Override - public final T visit(ColumnToColumnMapping columnMapping) { + public final R visit(ColumnToColumnMapping columnMapping) { throw new UnsupportedOperationException(); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/util/InsertMappingVisitor.java b/src/main/java/org/mybatis/dynamic/sql/util/InsertMappingVisitor.java index f365eaa05..8f844707e 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/InsertMappingVisitor.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/InsertMappingVisitor.java @@ -15,24 +15,24 @@ */ package org.mybatis.dynamic.sql.util; -public abstract class InsertMappingVisitor implements ColumnMappingVisitor { +public abstract class InsertMappingVisitor implements ColumnMappingVisitor { @Override - public final T visit(ValueMapping mapping) { + public final R visit(ValueMapping mapping) { throw new UnsupportedOperationException(); } @Override - public final T visit(ValueWhenPresentMapping mapping) { + public final R visit(ValueWhenPresentMapping mapping) { throw new UnsupportedOperationException(); } @Override - public final T visit(SelectMapping mapping) { + public final R visit(SelectMapping mapping) { throw new UnsupportedOperationException(); } @Override - public final T visit(ColumnToColumnMapping columnMapping) { + public final R visit(ColumnToColumnMapping columnMapping) { throw new UnsupportedOperationException(); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/util/MultiRowInsertMappingVisitor.java b/src/main/java/org/mybatis/dynamic/sql/util/MultiRowInsertMappingVisitor.java index a2765fd35..984fe305f 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/MultiRowInsertMappingVisitor.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/MultiRowInsertMappingVisitor.java @@ -15,9 +15,9 @@ */ package org.mybatis.dynamic.sql.util; -public abstract class MultiRowInsertMappingVisitor extends InsertMappingVisitor { +public abstract class MultiRowInsertMappingVisitor extends InsertMappingVisitor { @Override - public final T visit(PropertyWhenPresentMapping mapping) { + public final R visit(PropertyWhenPresentMapping mapping) { throw new UnsupportedOperationException(); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/util/NullMapping.java b/src/main/java/org/mybatis/dynamic/sql/util/NullMapping.java index c47041fa9..99ad6f856 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/NullMapping.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/NullMapping.java @@ -1,5 +1,5 @@ /** - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2020 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. diff --git a/src/main/java/org/mybatis/dynamic/sql/util/SelectMapping.java b/src/main/java/org/mybatis/dynamic/sql/util/SelectMapping.java index 7e43caf9a..e405871bb 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/SelectMapping.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/SelectMapping.java @@ -1,5 +1,5 @@ /** - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2020 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. diff --git a/src/main/java/org/mybatis/dynamic/sql/util/StringConstantMapping.java b/src/main/java/org/mybatis/dynamic/sql/util/StringConstantMapping.java index 32904bb53..0a088b5b3 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/StringConstantMapping.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/StringConstantMapping.java @@ -1,5 +1,5 @@ /** - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2020 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. diff --git a/src/main/java/org/mybatis/dynamic/sql/util/UpdateMappingVisitor.java b/src/main/java/org/mybatis/dynamic/sql/util/UpdateMappingVisitor.java index 2ec1391d6..79c721acf 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/UpdateMappingVisitor.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/UpdateMappingVisitor.java @@ -15,14 +15,14 @@ */ package org.mybatis.dynamic.sql.util; -public abstract class UpdateMappingVisitor implements ColumnMappingVisitor { +public abstract class UpdateMappingVisitor implements ColumnMappingVisitor { @Override - public final T visit(PropertyMapping mapping) { + public final R visit(PropertyMapping mapping) { throw new UnsupportedOperationException(); } @Override - public final T visit(PropertyWhenPresentMapping mapping) { + public final R visit(PropertyWhenPresentMapping mapping) { throw new UnsupportedOperationException(); } } diff --git a/src/main/java/org/mybatis/dynamic/sql/util/ValueMapping.java b/src/main/java/org/mybatis/dynamic/sql/util/ValueMapping.java index 18a9b8e4b..778b98798 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/ValueMapping.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/ValueMapping.java @@ -23,14 +23,17 @@ public class ValueMapping extends AbstractColumnMapping { private Supplier valueSupplier; + // keep a reference to the column so we don't lose the type + private SqlColumn localColumn; private ValueMapping(SqlColumn column, Supplier valueSupplier) { super(column); this.valueSupplier = Objects.requireNonNull(valueSupplier); + localColumn = Objects.requireNonNull(column); } - public T value() { - return valueSupplier.get(); + public Object value() { + return localColumn.convertParameterType(valueSupplier.get()); } @Override diff --git a/src/main/java/org/mybatis/dynamic/sql/util/ValueWhenPresentMapping.java b/src/main/java/org/mybatis/dynamic/sql/util/ValueWhenPresentMapping.java index 49644e700..671916eae 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/ValueWhenPresentMapping.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/ValueWhenPresentMapping.java @@ -24,14 +24,21 @@ public class ValueWhenPresentMapping extends AbstractColumnMapping { private Supplier valueSupplier; + // keep a reference to the column so we don't lose the type + private SqlColumn localColumn; private ValueWhenPresentMapping(SqlColumn column, Supplier valueSupplier) { super(column); this.valueSupplier = Objects.requireNonNull(valueSupplier); + localColumn = Objects.requireNonNull(column); } - public Optional value() { - return Optional.ofNullable(valueSupplier.get()); + public Optional value() { + return Optional.ofNullable(valueSupplier.get()).map(this::convert); + } + + private Object convert(T value) { + return localColumn.convertParameterType(value); } @Override diff --git a/src/main/java/org/mybatis/dynamic/sql/util/mybatis3/MyBatis3Utils.java b/src/main/java/org/mybatis/dynamic/sql/util/mybatis3/MyBatis3Utils.java index bd6db7aa9..3516702ad 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/mybatis3/MyBatis3Utils.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/mybatis3/MyBatis3Utils.java @@ -114,16 +114,16 @@ public static int insert(ToIntFunction> mapper, R return mapper.applyAsInt(insert(record, table, completer)); } - public static GeneralInsertStatementProvider insert(SqlTable table, + public static GeneralInsertStatementProvider generalInsert(SqlTable table, UnaryOperator completer) { return completer.apply(GeneralInsertDSL.insertInto(table)) .build() .render(RenderingStrategies.MYBATIS3); } - public static int insert(ToIntFunction mapper, + public static int generalInsert(ToIntFunction mapper, SqlTable table, UnaryOperator completer) { - return mapper.applyAsInt(insert(table, completer)); + return mapper.applyAsInt(generalInsert(table, completer)); } public static MultiRowInsertStatementProvider insertMultiple(Collection records, SqlTable table, diff --git a/src/main/java/org/mybatis/dynamic/sql/util/spring/NamedParameterJdbcTemplateExtensions.java b/src/main/java/org/mybatis/dynamic/sql/util/spring/NamedParameterJdbcTemplateExtensions.java new file mode 100644 index 000000000..507aaeafd --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/util/spring/NamedParameterJdbcTemplateExtensions.java @@ -0,0 +1,163 @@ +/** + * Copyright 2016-2020 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mybatis.dynamic.sql.util.spring; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import org.mybatis.dynamic.sql.delete.DeleteModel; +import org.mybatis.dynamic.sql.delete.render.DeleteStatementProvider; +import org.mybatis.dynamic.sql.insert.BatchInsertModel; +import org.mybatis.dynamic.sql.insert.GeneralInsertModel; +import org.mybatis.dynamic.sql.insert.InsertModel; +import org.mybatis.dynamic.sql.insert.MultiRowInsertModel; +import org.mybatis.dynamic.sql.insert.render.BatchInsert; +import org.mybatis.dynamic.sql.insert.render.GeneralInsertStatementProvider; +import org.mybatis.dynamic.sql.insert.render.InsertStatementProvider; +import org.mybatis.dynamic.sql.insert.render.MultiRowInsertStatementProvider; +import org.mybatis.dynamic.sql.render.RenderingStrategies; +import org.mybatis.dynamic.sql.select.SelectModel; +import org.mybatis.dynamic.sql.select.render.SelectStatementProvider; +import org.mybatis.dynamic.sql.update.UpdateModel; +import org.mybatis.dynamic.sql.update.render.UpdateStatementProvider; +import org.mybatis.dynamic.sql.util.Buildable; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.jdbc.core.namedparam.SqlParameterSourceUtils; +import org.springframework.jdbc.support.KeyHolder; + +public class NamedParameterJdbcTemplateExtensions { + private final NamedParameterJdbcTemplate template; + + public NamedParameterJdbcTemplateExtensions(NamedParameterJdbcTemplate template) { + this.template = Objects.requireNonNull(template); + } + + public long count(Buildable countStatement) { + return count(countStatement.build().render(RenderingStrategies.SPRING_NAMED_PARAMETER)); + } + + public long count(SelectStatementProvider countStatement) { + return template.queryForObject(countStatement.getSelectStatement(), countStatement.getParameters(), Long.class); + } + + public int delete(Buildable deleteStatement) { + return delete(deleteStatement.build().render(RenderingStrategies.SPRING_NAMED_PARAMETER)); + } + + public int delete(DeleteStatementProvider deleteStatement) { + return template.update(deleteStatement.getDeleteStatement(), deleteStatement.getParameters()); + } + + public int generalInsert(Buildable insertStatement) { + return generalInsert(insertStatement.build().render(RenderingStrategies.SPRING_NAMED_PARAMETER)); + } + + public int generalInsert(GeneralInsertStatementProvider insertStatement) { + return template.update(insertStatement.getInsertStatement(), insertStatement.getParameters()); + } + + public int generalInsert(Buildable insertStatement, KeyHolder keyHolder) { + return generalInsert(insertStatement.build().render(RenderingStrategies.SPRING_NAMED_PARAMETER), keyHolder); + } + + public int generalInsert(GeneralInsertStatementProvider insertStatement, KeyHolder keyHolder) { + return template.update(insertStatement.getInsertStatement(), + new MapSqlParameterSource(insertStatement.getParameters()), keyHolder); + } + + public int insert(Buildable> insertStatement) { + return insert(insertStatement.build().render(RenderingStrategies.SPRING_NAMED_PARAMETER)); + } + + public int insert(InsertStatementProvider insertStatement) { + return template.update(insertStatement.getInsertStatement(), + new BeanPropertySqlParameterSource(insertStatement.getRecord())); + } + + public int insert(Buildable> insertStatement, KeyHolder keyHolder) { + return insert(insertStatement.build().render(RenderingStrategies.SPRING_NAMED_PARAMETER), keyHolder); + } + + public int insert(InsertStatementProvider insertStatement, KeyHolder keyHolder) { + return template.update(insertStatement.getInsertStatement(), + new BeanPropertySqlParameterSource(insertStatement.getRecord()), keyHolder); + } + + public int[] insertBatch(Buildable> insertStatement) { + return insertBatch(insertStatement.build().render(RenderingStrategies.SPRING_NAMED_PARAMETER)); + } + + public int[] insertBatch(BatchInsert insertStatement) { + SqlParameterSource[] batch = SqlParameterSourceUtils.createBatch(insertStatement.getRecords()); + return template.batchUpdate(insertStatement.getInsertStatementSQL(), batch); + } + + public int insertMultiple(Buildable> insertStatement) { + return insertMultiple(insertStatement.build().render(RenderingStrategies.SPRING_NAMED_PARAMETER)); + } + + public int insertMultiple(MultiRowInsertStatementProvider insertStatement) { + return template.update(insertStatement.getInsertStatement(), + new BeanPropertySqlParameterSource(insertStatement)); + } + + public int insertMultiple(Buildable> insertStatement, KeyHolder keyHolder) { + return insertMultiple(insertStatement.build().render(RenderingStrategies.SPRING_NAMED_PARAMETER), keyHolder); + } + + public int insertMultiple(MultiRowInsertStatementProvider insertStatement, KeyHolder keyHolder) { + return template.update(insertStatement.getInsertStatement(), + new BeanPropertySqlParameterSource(insertStatement), keyHolder); + } + + public List selectList(Buildable selectStatement, RowMapper rowMapper) { + return selectList(selectStatement.build().render(RenderingStrategies.SPRING_NAMED_PARAMETER), rowMapper); + } + + public List selectList(SelectStatementProvider selectStatement, RowMapper rowMapper) { + return template.query(selectStatement.getSelectStatement(), selectStatement.getParameters(), rowMapper); + } + + public Optional selectOne(Buildable selectStatement, RowMapper rowMapper) { + return selectOne(selectStatement.build().render(RenderingStrategies.SPRING_NAMED_PARAMETER), rowMapper); + } + + public Optional selectOne(SelectStatementProvider selectStatement, RowMapper rowMapper) { + T result; + try { + result = template.queryForObject(selectStatement.getSelectStatement(), selectStatement.getParameters(), + rowMapper); + } catch (EmptyResultDataAccessException e) { + result = null; + } + + return Optional.ofNullable(result); + } + + public int update(Buildable updateStatement) { + return update(updateStatement.build().render(RenderingStrategies.SPRING_NAMED_PARAMETER)); + } + + public int update(UpdateStatementProvider updateStatement) { + return template.update(updateStatement.getUpdateStatement(), updateStatement.getParameters()); + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/where/render/WhereConditionVisitor.java b/src/main/java/org/mybatis/dynamic/sql/where/render/WhereConditionVisitor.java index ab294b3f8..75708d1f6 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/render/WhereConditionVisitor.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/render/WhereConditionVisitor.java @@ -77,7 +77,7 @@ public Optional visit(AbstractSingleValueCondition con getFormattedJdbcPlaceholder(mapKey)); return FragmentAndParameters.withFragment(fragment) - .withParameter(mapKey, condition.value()) + .withParameter(mapKey, convertValue(condition.value())) .buildOptional(); } @@ -90,8 +90,8 @@ public Optional visit(AbstractTwoValueCondition condit getFormattedJdbcPlaceholder(mapKey2)); return FragmentAndParameters.withFragment(fragment) - .withParameter(mapKey1, condition.value1()) - .withParameter(mapKey2, condition.value2()) + .withParameter(mapKey1, convertValue(condition.value1())) + .withParameter(mapKey2, convertValue(condition.value2())) .buildOptional(); } @@ -117,14 +117,18 @@ public Optional visit(AbstractColumnComparisonCondition Buildable + +typealias InsertCompleter = @MyBatisDslMarker InsertDSL.() -> Buildable> + +typealias MultiRowInsertCompleter = @MyBatisDslMarker MultiRowInsertDSL.() -> Buildable> + +typealias BatchInsertCompleter = @MyBatisDslMarker BatchInsertDSL.() -> Buildable> diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinUpdateBuilder.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinUpdateBuilder.kt index e2f926886..f76078110 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinUpdateBuilder.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinUpdateBuilder.kt @@ -16,23 +16,10 @@ package org.mybatis.dynamic.sql.util.kotlin import org.mybatis.dynamic.sql.SqlColumn -import org.mybatis.dynamic.sql.insert.GeneralInsertDSL -import org.mybatis.dynamic.sql.insert.GeneralInsertModel -import org.mybatis.dynamic.sql.insert.InsertDSL -import org.mybatis.dynamic.sql.insert.InsertModel -import org.mybatis.dynamic.sql.insert.MultiRowInsertDSL -import org.mybatis.dynamic.sql.insert.MultiRowInsertModel import org.mybatis.dynamic.sql.update.UpdateDSL import org.mybatis.dynamic.sql.update.UpdateModel import org.mybatis.dynamic.sql.util.Buildable -// insert completers are here because sonar doesn't see them as covered if they are in a file by themselves -typealias GeneralInsertCompleter = GeneralInsertDSL.() -> Buildable - -typealias InsertCompleter = InsertDSL.() -> Buildable> - -typealias MultiRowInsertCompleter = MultiRowInsertDSL.() -> Buildable> - typealias UpdateCompleter = KotlinUpdateBuilder.() -> Buildable class KotlinUpdateBuilder(private val dsl: UpdateDSL) : diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/mybatis3/ProviderBuilderFunctions.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/mybatis3/ProviderBuilderFunctions.kt index 2ef3b1daf..4c8c7606c 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/mybatis3/ProviderBuilderFunctions.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/mybatis3/ProviderBuilderFunctions.kt @@ -42,6 +42,9 @@ fun countFrom(table: SqlTable, completer: CountCompleter) = fun deleteFrom(table: SqlTable, completer: DeleteCompleter) = completer(KotlinDeleteBuilder(SqlBuilder.deleteFrom(table))).build().render(RenderingStrategies.MYBATIS3) +fun insertInto(table: SqlTable, completer: GeneralInsertCompleter) = + completer(GeneralInsertDSL.insertInto(table)).build().render(RenderingStrategies.MYBATIS3) + fun InsertDSL.IntoGatherer.into(table: SqlTable, completer: InsertCompleter) = completer(into(table)).build().render(RenderingStrategies.MYBATIS3) @@ -62,6 +65,3 @@ fun select(start: QueryExpressionDSL, completer: SelectCompleter) = fun update(table: SqlTable, completer: UpdateCompleter) = completer(KotlinUpdateBuilder(SqlBuilder.update(table))).build().render(RenderingStrategies.MYBATIS3) - -fun insertInto(table: SqlTable, completer: GeneralInsertCompleter) = - completer(GeneralInsertDSL.insertInto(table)).build().render(RenderingStrategies.MYBATIS3) diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/spring/NamedParameterJdbcTemplateExtensions.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/spring/NamedParameterJdbcTemplateExtensions.kt index 7f751492b..829133748 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/spring/NamedParameterJdbcTemplateExtensions.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/spring/NamedParameterJdbcTemplateExtensions.kt @@ -14,25 +14,35 @@ * limitations under the License. */ @file:Suppress("TooManyFunctions") + package org.mybatis.dynamic.sql.util.kotlin.spring import org.mybatis.dynamic.sql.BasicColumn import org.mybatis.dynamic.sql.SqlBuilder import org.mybatis.dynamic.sql.SqlTable import org.mybatis.dynamic.sql.delete.render.DeleteStatementProvider +import org.mybatis.dynamic.sql.insert.render.BatchInsert import org.mybatis.dynamic.sql.insert.render.GeneralInsertStatementProvider import org.mybatis.dynamic.sql.insert.render.InsertStatementProvider +import org.mybatis.dynamic.sql.insert.render.MultiRowInsertStatementProvider import org.mybatis.dynamic.sql.select.CountDSL import org.mybatis.dynamic.sql.select.render.SelectStatementProvider import org.mybatis.dynamic.sql.update.render.UpdateStatementProvider +import org.mybatis.dynamic.sql.util.kotlin.BatchInsertCompleter import org.mybatis.dynamic.sql.util.kotlin.CountCompleter import org.mybatis.dynamic.sql.util.kotlin.DeleteCompleter import org.mybatis.dynamic.sql.util.kotlin.GeneralInsertCompleter import org.mybatis.dynamic.sql.util.kotlin.InsertCompleter +import org.mybatis.dynamic.sql.util.kotlin.MultiRowInsertCompleter +import org.mybatis.dynamic.sql.util.kotlin.MyBatisDslMarker import org.mybatis.dynamic.sql.util.kotlin.SelectCompleter import org.mybatis.dynamic.sql.util.kotlin.UpdateCompleter +import org.springframework.dao.EmptyResultDataAccessException import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate +import org.springframework.jdbc.core.namedparam.SqlParameterSourceUtils +import org.springframework.jdbc.support.KeyHolder import java.sql.ResultSet fun NamedParameterJdbcTemplate.count(selectStatement: SelectStatementProvider) = @@ -53,17 +63,62 @@ fun NamedParameterJdbcTemplate.delete(deleteStatement: DeleteStatementProvider) fun NamedParameterJdbcTemplate.deleteFrom(table: SqlTable, completer: DeleteCompleter) = delete(org.mybatis.dynamic.sql.util.kotlin.spring.deleteFrom(table, completer)) +// batch insert +fun NamedParameterJdbcTemplate.insertBatch(insertStatement: BatchInsert): IntArray = + batchUpdate(insertStatement.insertStatementSQL, SqlParameterSourceUtils.createBatch(insertStatement.records)) + +fun NamedParameterJdbcTemplate.insertBatch(vararg records: T) = + insertBatch(records.asList()) + +fun NamedParameterJdbcTemplate.insertBatch(records: List) = + BatchInsertHelper(records, this) + +// single record insert fun NamedParameterJdbcTemplate.insert(insertStatement: InsertStatementProvider) = update(insertStatement.insertStatement, BeanPropertySqlParameterSource(insertStatement.record)) +fun NamedParameterJdbcTemplate.insert(insertStatement: InsertStatementProvider, keyHolder: KeyHolder) = + update(insertStatement.insertStatement, BeanPropertySqlParameterSource(insertStatement.record), keyHolder) + +fun NamedParameterJdbcTemplate.insert(record: T) = + SingleRowInsertHelper(record, this) + +@Deprecated( + message = "Deprecated for being awkward and inconsistent", + replaceWith = ReplaceWith("insert(record).into(table, completer)") +) fun NamedParameterJdbcTemplate.insert(record: T, table: SqlTable, completer: InsertCompleter) = insert(SqlBuilder.insert(record).into(table, completer)) -fun NamedParameterJdbcTemplate.insert(insertStatement: GeneralInsertStatementProvider) = +// general insert +fun NamedParameterJdbcTemplate.generalInsert(insertStatement: GeneralInsertStatementProvider) = update(insertStatement.insertStatement, insertStatement.parameters) +fun NamedParameterJdbcTemplate.generalInsert(insertStatement: GeneralInsertStatementProvider, keyHolder: KeyHolder) = + update(insertStatement.insertStatement, MapSqlParameterSource(insertStatement.parameters), keyHolder) + fun NamedParameterJdbcTemplate.insertInto(table: SqlTable, completer: GeneralInsertCompleter) = - insert(org.mybatis.dynamic.sql.util.kotlin.spring.insertInto(table, completer)) + generalInsert(org.mybatis.dynamic.sql.util.kotlin.spring.insertInto(table, completer)) + +// multiple record insert +fun NamedParameterJdbcTemplate.insertMultiple(vararg records: T) = + insertMultiple(records.asList()) + +fun NamedParameterJdbcTemplate.insertMultiple(records: List) = + MultiRowInsertHelper(records, this) + +fun NamedParameterJdbcTemplate.insertMultiple(insertStatement: MultiRowInsertStatementProvider) = + update(insertStatement.insertStatement, BeanPropertySqlParameterSource(insertStatement)) + +fun NamedParameterJdbcTemplate.insertMultiple( + insertStatement: MultiRowInsertStatementProvider, + keyHolder: KeyHolder +) = + update(insertStatement.insertStatement, BeanPropertySqlParameterSource(insertStatement), keyHolder) + +// insert with KeyHolder support +fun NamedParameterJdbcTemplate.withKeyHolder(keyHolder: KeyHolder, build: KeyHolderHelper.() -> Int) = + build(KeyHolderHelper(keyHolder, this)) fun NamedParameterJdbcTemplate.select(vararg selectList: BasicColumn) = SelectListFromGatherer(selectList.toList(), this) @@ -83,8 +138,11 @@ fun NamedParameterJdbcTemplate.selectList( fun NamedParameterJdbcTemplate.selectOne( selectStatement: SelectStatementProvider, rowMapper: (rs: ResultSet, rowNum: Int) -> T -): T? = +): T? = try { queryForObject(selectStatement.selectStatement, selectStatement.parameters, rowMapper) +} catch (e: EmptyResultDataAccessException) { + null +} fun NamedParameterJdbcTemplate.update(updateStatement: UpdateStatementProvider) = update(updateStatement.updateStatement, updateStatement.parameters) @@ -158,3 +216,46 @@ class SelectOneMapperGatherer( fun withRowMapper(rowMapper: (rs: ResultSet, rowNum: Int) -> T) = template.selectOne(selectStatement, rowMapper) } + +@MyBatisDslMarker +class KeyHolderHelper(private val keyHolder: KeyHolder, private val template: NamedParameterJdbcTemplate) { + fun insertInto(table: SqlTable, completer: GeneralInsertCompleter) = + template.generalInsert(org.mybatis.dynamic.sql.util.kotlin.spring.insertInto(table, completer), keyHolder) + + fun insert(record: T) = + SingleRowInsertHelper(record, template, keyHolder) + + fun insertMultiple(vararg records: T) = + insertMultiple(records.asList()) + + fun insertMultiple(records: List) = + MultiRowInsertHelper(records, template, keyHolder) +} + +@MyBatisDslMarker +class BatchInsertHelper(private val records: List, private val template: NamedParameterJdbcTemplate) { + fun into(table: SqlTable, completer: BatchInsertCompleter) = + template.insertBatch(SqlBuilder.insertBatch(records).into(table, completer)) +} + +@MyBatisDslMarker +class MultiRowInsertHelper( + private val records: List, private val template: NamedParameterJdbcTemplate, + private val keyHolder: KeyHolder? = null +) { + fun into(table: SqlTable, completer: MultiRowInsertCompleter) = + with(SqlBuilder.insertMultiple(records).into(table, completer)) { + keyHolder?.let { template.insertMultiple(this, it) } ?: template.insertMultiple(this) + } +} + +@MyBatisDslMarker +class SingleRowInsertHelper( + private val record: T, private val template: NamedParameterJdbcTemplate, + private val keyHolder: KeyHolder? = null +) { + fun into(table: SqlTable, completer: InsertCompleter) = + with(SqlBuilder.insert(record).into(table, completer)) { + keyHolder?.let { template.insert(this, it) } ?: template.insert(this) + } +} diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/spring/ProviderBuilderFunctions.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/spring/ProviderBuilderFunctions.kt index b8cf8cee3..7646c2459 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/spring/ProviderBuilderFunctions.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/spring/ProviderBuilderFunctions.kt @@ -17,12 +17,15 @@ package org.mybatis.dynamic.sql.util.kotlin.spring import org.mybatis.dynamic.sql.SqlBuilder import org.mybatis.dynamic.sql.SqlTable +import org.mybatis.dynamic.sql.insert.BatchInsertDSL import org.mybatis.dynamic.sql.insert.GeneralInsertDSL import org.mybatis.dynamic.sql.insert.InsertDSL +import org.mybatis.dynamic.sql.insert.MultiRowInsertDSL import org.mybatis.dynamic.sql.render.RenderingStrategies import org.mybatis.dynamic.sql.select.CountDSL import org.mybatis.dynamic.sql.select.QueryExpressionDSL import org.mybatis.dynamic.sql.select.SelectModel +import org.mybatis.dynamic.sql.util.kotlin.BatchInsertCompleter import org.mybatis.dynamic.sql.util.kotlin.CountCompleter import org.mybatis.dynamic.sql.util.kotlin.DeleteCompleter import org.mybatis.dynamic.sql.util.kotlin.GeneralInsertCompleter @@ -31,6 +34,7 @@ import org.mybatis.dynamic.sql.util.kotlin.KotlinCountBuilder import org.mybatis.dynamic.sql.util.kotlin.KotlinDeleteBuilder import org.mybatis.dynamic.sql.util.kotlin.KotlinQueryBuilder import org.mybatis.dynamic.sql.util.kotlin.KotlinUpdateBuilder +import org.mybatis.dynamic.sql.util.kotlin.MultiRowInsertCompleter import org.mybatis.dynamic.sql.util.kotlin.SelectCompleter import org.mybatis.dynamic.sql.util.kotlin.UpdateCompleter @@ -42,9 +46,18 @@ fun deleteFrom(table: SqlTable, completer: DeleteCompleter) = completer(KotlinDeleteBuilder(SqlBuilder.deleteFrom(table))).build() .render(RenderingStrategies.SPRING_NAMED_PARAMETER) +fun insertInto(table: SqlTable, completer: GeneralInsertCompleter) = + completer(GeneralInsertDSL.insertInto(table)).build().render(RenderingStrategies.SPRING_NAMED_PARAMETER) + +fun BatchInsertDSL.IntoGatherer.into(table: SqlTable, completer: BatchInsertCompleter) = + completer(into(table)).build().render(RenderingStrategies.SPRING_NAMED_PARAMETER) + fun InsertDSL.IntoGatherer.into(table: SqlTable, completer: InsertCompleter) = completer(into(table)).build().render(RenderingStrategies.SPRING_NAMED_PARAMETER) +fun MultiRowInsertDSL.IntoGatherer.into(table: SqlTable, completer: MultiRowInsertCompleter) = + completer(into(table)).build().render(RenderingStrategies.SPRING_NAMED_PARAMETER) + fun CountDSL.FromGatherer.from(table: SqlTable, completer: CountCompleter) = completer(KotlinCountBuilder(from(table))).build().render(RenderingStrategies.SPRING_NAMED_PARAMETER) @@ -56,6 +69,3 @@ fun QueryExpressionDSL.FromGatherer.from(table: SqlTable, alias: St fun update(table: SqlTable, completer: UpdateCompleter) = completer(KotlinUpdateBuilder(SqlBuilder.update(table))).build().render(RenderingStrategies.SPRING_NAMED_PARAMETER) - -fun insertInto(table: SqlTable, completer: GeneralInsertCompleter) = - completer(GeneralInsertDSL.insertInto(table)).build().render(RenderingStrategies.SPRING_NAMED_PARAMETER) diff --git a/src/site/markdown/docs/codingStandards.md b/src/site/markdown/docs/codingStandards.md index 29a26d860..052aa0c61 100644 --- a/src/site/markdown/docs/codingStandards.md +++ b/src/site/markdown/docs/codingStandards.md @@ -20,8 +20,8 @@ these general principles for functional style coding in Java: - Classes never expose a modifiable Map. A Map may be exposed with an unmodifiable Map. - Avoid direct use of null. Any Class attribute that could be null in normal use should be wrapped in a `java.util.Optional` - Avoid for loops (imperative) - use map/filter/reduce/collect (declarative) instead -- Avoid Stream.forEach() - this method is only used for side effects, and we want no side-effects -- Avoid Optional.ifPresent() - this method is only used for side effects, and we want no side-effects +- Avoid Stream.forEach() - this method is only used for side effects, and we want no side effects +- Avoid Optional.ifPresent() - this method is only used for side effects, and we want no side effects - The only good function is a pure function. Some functions in the library accept an AtomicInteger which is a necessary evil - Classes with no internal attributes are usually a collection of utility functions. Use static methods in an interface instead. - Remember the single responsibility principle - methods do one thing, classes have one responsibility @@ -43,7 +43,7 @@ We are committed to clean code. This means: Remember the three rules of TDD: 1. You may not write production code until you have written a failing unit test. -2. You may not write more of a unit test that is sufficient to fail, and not compiling is failing. +2. You may not write more of a unit test than is sufficient to fail, and not compiling is failing. 3. You may not write more production code than is sufficient to passing the currently failing test. diff --git a/src/site/markdown/docs/complexQueries.md b/src/site/markdown/docs/complexQueries.md index f603727e0..3d87ed8d9 100644 --- a/src/site/markdown/docs/complexQueries.md +++ b/src/site/markdown/docs/complexQueries.md @@ -1,5 +1,5 @@ # Complex Queries -Enhancements in version 1.1.2 make it easier to code complex queries. The Select DSL is implemented as a set of related objects. As the select statement is built, intermediate objects of various types are returned from the various methods that implement the DSL. The select statement can be completed by calling the `build()` method many of the intermediate objects. Prior to version 1.1.2, it was necessary to call `build()` on the **last** intermediate object. This restriction has been removed and it is now possible to call `build()` on **any** intermediate object. This, along with several other enhancements, has simplified the coding of complex queries. +Enhancements in version 1.1.2 make it easier to code complex queries. The Select DSL is implemented as a set of related objects. As the select statement is built, intermediate objects of various types are returned from the various methods that implement the DSL. The select statement can be completed by calling the `build()` method many of the intermediate objects. Prior to version 1.1.2, it was necessary to call `build()` on the **last** intermediate object. This restriction has been removed, and it is now possible to call `build()` on **any** intermediate object. This, along with several other enhancements, has simplified the coding of complex queries. For example, suppose you want to code a complex search on a Person table. The search parameters are id, first name, and last name. The rules are: @@ -39,7 +39,7 @@ Notes: 1. Note the use of the `var` keyword here. If you are using an older version of Java, the actual type is `QueryExpressionDSL.QueryExpressionWhereBuilder` 1. Here we are calling `where()` with no parameters. This sets up the builder to accept conditions further along in the code. If no conditions are added, then the where clause will not be rendered -1. This `if` statement implements the rules of the search. If an ID is entered , use it. Otherwise do a fuzzy search based on first name and last name. +1. This `if` statement implements the rules of the search. If an ID is entered , use it. Otherwise, do a fuzzy search based on first name and last name. 1. The `then` statement on this line allows you to change the parameter value before it is placed in the parameter Map. In this case we are adding SQL wildcards to the start and end of the search String - but only if the search String is not null. If the search String is null, the lambda will not be called and the condition will not render 1. This shows using a method reference instead of a lambda on the `then`. Method references allow you to more clearly express intent. Note also the use of the `isLikeWhenPresent` condition which is a built in condition that checks for nulls 1. It is a good idea to limit the number of rows returned from a search. The library now supports `fetch first` syntax for limiting rows diff --git a/src/site/markdown/docs/conditions.md b/src/site/markdown/docs/conditions.md index 71648bb7d..435a7ab23 100644 --- a/src/site/markdown/docs/conditions.md +++ b/src/site/markdown/docs/conditions.md @@ -135,11 +135,11 @@ The following table shows the different supplied In conditions and how they will | IsIn| No | No| name in ('foo', null, 'bar') | name in (null) | | IsInWhenPresent | Yes | No | name in ('foo', 'bar') | No Render | | IsInCaseInsensitive | No | Yes | upper(name) in ('FOO', null, 'BAR') | upper(name) in (null) | -| IsInCaseInsensiteveWhenPresent | Yes | Yes | upper(name) in ('FOO', 'BAR') | No Render | +| IsInCaseInsensitiveWhenPresent | Yes | Yes | upper(name) in ('FOO', 'BAR') | No Render | | IsNotIn| No | No| name not in ('foo', null, 'bar') | name not in (null) | | IsNotInWhenPresent | Yes | No | name not in ('foo', 'bar') | No render | | IsNotInCaseInsensitive | No | Yes | upper(name) not in ('FOO', null, 'BAR') | upper(name) not in (null) | -| IsNotInCaseInsensiteveWhenPresent | Yes | Yes | upper(name) not in ('FOO', 'BAR') | No Render | +| IsNotInCaseInsensitiveWhenPresent | Yes | Yes | upper(name) not in ('FOO', 'BAR') | No Render | If none of these options meet your needs, there is an extension point where you can add your own filter and/or map conditions to the value stream. This gives you great flexibility to alter or filter the value list before the condition is rendered. @@ -184,4 +184,4 @@ Then the condition could be used in a query as follows: .render(RenderingStrategies.MYBATIS3); ``` -You can apply value stream operations to the conditions `IsIn`, `IsInCaseInsensitive`, `IsNotIn`, and `IsNotInCaseInsensitive`. With the case insensitive conditions, the library will automatically convert non-null strings to upper case after any value stream operation you specify. +You can apply value stream operations to the conditions `IsIn`, `IsInCaseInsensitive`, `IsNotIn`, and `IsNotInCaseInsensitive`. With the case-insensitive conditions, the library will automatically convert non-null strings to upper case after any value stream operation you specify. diff --git a/src/site/markdown/docs/databaseObjects.md b/src/site/markdown/docs/databaseObjects.md index 7bbe3739d..2d217c0c8 100644 --- a/src/site/markdown/docs/databaseObjects.md +++ b/src/site/markdown/docs/databaseObjects.md @@ -130,9 +130,9 @@ Whenever the table is referenced for rendering SQL, the name will be calculated ## Column Representation -The class `org.mybatis.dynamic.sql.SqlColumn` is used to represent a column in a table or view. An `SqlColumn` is always associated with a `SqlTable`. In it's most basic form, the `SqlColumn` class holds a name and a reference to the `SqlTable` it is associated with. The table reference is required so that table aliases can be applied to columns in the rendering phase. +The class `org.mybatis.dynamic.sql.SqlColumn` is used to represent a column in a table or view. An `SqlColumn` is always associated with a `SqlTable`. In its most basic form, the `SqlColumn` class holds a name and a reference to the `SqlTable` it is associated with. The table reference is required so that table aliases can be applied to columns in the rendering phase. -The `SqlColumn` will be rendered in SQL based on the `RenderingStrategy` applied to the SQL statement. Typically the rendering strategy generates a string that represents a parameter marker in whatever SQL engine you are using. For example, MyBatis3 parameter markers are formatted as "#{some_attribute}". By default all columns are rendered with the same strategy. The library supplies rendering strategies that are appropriate for several SQL execution engines including MyBatis3 and Spring JDBC template. +The `SqlColumn` will be rendered in SQL based on the `RenderingStrategy` applied to the SQL statement. Typically the rendering strategy generates a string that represents a parameter marker in whatever SQL engine you are using. For example, MyBatis3 parameter markers are formatted as "#{some_attribute}". By default, all columns are rendered with the same strategy. The library supplies rendering strategies that are appropriate for several SQL execution engines including MyBatis3 and Spring JDBC template. In some cases it is necessary to override the rendering strategy for a particular column - so the `SqlColumn` class supports specifying a rendering strategy for a column that will override the rendering strategy applied to a statement. A good example of this use case is with PostgreSQL. In that database it is required to add the string "::jsonb" to a prepared statement parameter marker when inserting or updating JSON fields, but not for other fields. A column based rendering strategy enables this. diff --git a/src/site/markdown/docs/extending.md b/src/site/markdown/docs/extending.md index c04a572ab..2f593f6d6 100644 --- a/src/site/markdown/docs/extending.md +++ b/src/site/markdown/docs/extending.md @@ -48,7 +48,7 @@ public class CountAll implements BasicColumn { This class is used to implement the `count(*)` function in a SELECT list. There are only three methods to implement: -1. `renderWithTableAlias` - the default renderers will write the value returned from this function into the select list - or the GROUP BY expression. If your item can be altered by a table alias, then here is where you change the return value based on the alias specified by the user. For a `count(*)` expression, a table alias never applies, so we just return the same value whether or not an alias has been specified by the user. +1. `renderWithTableAlias` - the default renderers will write the value returned from this function into the select list - or the GROUP BY expression. If your item can be altered by a table alias, then here is where you change the return value based on the alias specified by the user. For a `count(*)` expression, a table alias never applies, so we just return the same value regardless of whether an alias has been specified by the user. 2. `as` - this method can be called by the user to add an alias to the column expression. In the method you should return a new instance of the object, with the alias passed by the user. 3. `alias` - this method is called by the default renderer to obtain the column alias for the select list. If there is no alias, then returning Optional.empty() will disable setting a column alias. @@ -56,7 +56,7 @@ This class is used to implement the `count(*)` function in a SELECT list. There Relational database vendors provide hundreds of functions in their SQL dialects to aid with queries and offload processing to the database servers. This library does not try to implement every function that exists. This library also does not provide any abstraction over the different functions on different databases. For example, bitwise operator support is non-standard and it would be difficult to provide a function in this library that worked on every database. So we take the approach of supplying examples for a few very common functions, and making it relatively easy to write your own functions. -The supplied functions are all in the `org.mybatis.dynamic.sql.select.function` package. They are all implemented as `BindableColumn` - meaning that they can appear in a select list or a where clause. +The supplied functions are all in the `org.mybatis.dynamic.sql.select.function` package. They are all implemented as `BindableColumn` - meaning they can appear in a select list or a where clause. We provide some base classes that you can easily extend to write functions of your own. Those classes are as follows: @@ -190,10 +190,10 @@ The library will pass the following parameters to the `getFormattedJdbcPlacehold 1. `column` - the column definition for the current placeholder 2. `prefix` - For INSERT statements the value will be "record", for all other statements (including inserts with selects) the value will be "parameters" -3. `parameterName` - this will be the unique name for the the parameter. For INSERT statements, the name will be the property of the inserted record that is mapped to this parameter. For all other statements (including inserts with selects) a unique name will be generated by the library. That unique name will also be used to place the value of the parameter into the parameters Map. +3. `parameterName` - this will be the unique name for the parameter. For INSERT statements, the name will be the property of the inserted record mapped to this parameter. For all other statements (including inserts with selects) a unique name will be generated by the library. That unique name will also be used to place the value of the parameter into the parameters Map. ## Writing Custom Renderers -SQL rendering is accomplished by classes that are decoupled from the SQL model classes. All the model classes have a `render` method that calls the built-in default renderers, but this is completely optional and you do not need to use it. You can write your own rendering support if you are dissatisfied with the SQL produced by the default renderers. +SQL rendering is accomplished by classes that are decoupled from the SQL model classes. All the model classes have a `render` method that calls the built-in default renderers, but this is completely optional, and you do not need to use it. You can write your own rendering support if you are dissatisfied with the SQL produced by the default renderers. -Writing a custom renderer is quite complex. If you want to undertake that task, we suggest that you take the time to understand how the default renderers work first. And feel free to ask questions about this topic on the MyBatis mailing list. +Writing a custom renderer is quite complex. If you want to undertake that task, we suggest that you take the time to understand how the default renderers work first. Feel free to ask questions about this topic on the MyBatis mailing list. diff --git a/src/site/markdown/docs/functions.md b/src/site/markdown/docs/functions.md index 7613c0f6e..413d02840 100644 --- a/src/site/markdown/docs/functions.md +++ b/src/site/markdown/docs/functions.md @@ -1,6 +1,6 @@ # Database Functions -The library supplies implementations for several common database functions. We do not try to implement a large set of functions. Rather we supply some common functions as examples and make it relatively easy to write your own implementations of functions you might want to use that are not supplied by the library. See the page "Extending the Library" for informtion about how to write your own functions. +The library supplies implementations for several common database functions. We do not try to implement a large set of functions. Rather, we supply some common functions as examples and make it relatively easy to write your own implementations of functions you might want to use that are not supplied by the library. See the page "Extending the Library" for information about how to write your own functions. The supplied functions are all in the `org.mybatis.dynamic.sql.select.function` package. In addition, there are static methods in the `SqlBuilder` to access all functions. diff --git a/src/site/markdown/docs/howItWorks.md b/src/site/markdown/docs/howItWorks.md index 218475db6..40d3568e6 100644 --- a/src/site/markdown/docs/howItWorks.md +++ b/src/site/markdown/docs/howItWorks.md @@ -2,7 +2,7 @@ MyBatis does four main things: -1. It executes SQL in a safe way and abstracts away all the intricacies of JDBC +1. It executes SQL safely and abstracts away all the intricacies of JDBC 2. It maps parameter objects to JDBC prepared statement parameters 3. It maps rows in JDBC result sets to objects 4. It can generate dynamic SQL with special tags in XML, or through the use of various templating engines @@ -80,7 +80,7 @@ is designed to be the one single parameter for a MyBatis mapper method. ## What About SQL Injection? -It is true that mappers written this way are open to SQL injection. But this is also true of using any of the +It is true that mappers written this way are open to SQL injection. This is also true of using any of the various SQL provider classes in MyBatis (`@SelectProvider`, etc.) So you must be careful that these types of mappers are not exposed to any general user input. If you follow these practices, you will lower the risk of SQL injection: diff --git a/src/site/markdown/docs/insert.md b/src/site/markdown/docs/insert.md index 6e5af734c..012094a3f 100644 --- a/src/site/markdown/docs/insert.md +++ b/src/site/markdown/docs/insert.md @@ -97,8 +97,8 @@ The important thing is that the `keyProperty` is set correctly. It should alway ## Multiple Row Insert Support A multiple row insert is a single insert statement that inserts multiple rows into a table. This can be a convenient way to insert a few rows into a table, but it has some limitations: -1. Since it is a single SQL statement, you could generate quite a lot of prepared statement parameters. For example, suppose you wanted to insert 1000 records into a table, and each record had 5 fields. With a multiple row insert you would generate a SQL statement with 5000 parameters. There are limits to the number of parameters allowed in a JDBC prepared statement - and this kind of insert could easily exceed those limits. If you want to insert a large number of records, you should probably use a JDBC batch insert instead (see below) -1. The performance of a giant insert statement may be less than you expect. If you have a large number of records to insert, it will almost always be more efficient to use a JDBC batch insert (see below). With a batch insert, the JDBC driver can do some optimization that is not possible with a single large statement +1. Since it is a single SQL statement, you could generate quite a lot of prepared statement parameters. For example, suppose you wanted to insert 1000 records into a table, and each record had 5 fields. With a multiple row insert you would generate a SQL statement with 5000 parameters. There are limits to the number of parameters allowed in a JDBC prepared statement - and this kind of insert could easily exceed those limits. If you want to insert many records, you should probably use a JDBC batch insert instead (see below) +1. The performance of a giant insert statement may be less than you expect. If you have many records to insert, it will almost always be more efficient to use a JDBC batch insert (see below). With a batch insert, the JDBC driver can do some optimization that is not possible with a single large statement 1. Retrieving generated values with multiple row inserts can be a challenge. MyBatis currently has some limitations related to retrieving generated keys in multiple row inserts that require special considerations (see below) Nevertheless, there are use cases for a multiple row insert - especially when you just want to insert a few records in a table and don't need to retrieve generated keys. In those situations, a multiple row insert will be an easy solution. @@ -215,7 +215,7 @@ It is important to open a MyBatis session by setting the executor type to BATCH. Notice that the same mapper method that is used to insert a single record is now executed multiple times. The `map` methods are the same with the exception that the `toPropertyWhenPresent` mapping is not supported for batch inserts. ## General Insert Statement -A general insert is used to build arbitrary insert statements. The general insert does not require a separate record object o hold values for the statement - any value can be passed into the statement. This version of the insert is not convienient for retriving generated keys with MyBatis - for that use case we recommend the "single record insert". However the general insert is perfectly acceptible for Spring JDBC template or MyBatis inserts that do not return generated keys. For example +A general insert is used to build arbitrary insert statements. The general insert does not require a separate record object to hold values for the statement - any value can be passed into the statement. This version of the insert is not convenient for retrieving generated keys with MyBatis - for that use case we recommend the "single record insert". However the general insert is perfectly acceptable for Spring JDBC template or MyBatis inserts that do not return generated keys. For example ```java GeneralInsertStatementProvider insertStatement = insertInto(animalData) @@ -275,7 +275,7 @@ The XML element should look like this: ## Insert with Select -An insert select is an SQL insert statement the inserts the results of a select. For example: +An insert select is an SQL insert statement the inserts the results of a select statement. For example: ```java InsertSelectStatementProvider insertSelectStatement = insertInto(animalDataCopy) diff --git a/src/site/markdown/docs/introduction.md b/src/site/markdown/docs/introduction.md index 4b0ff10d8..45c789de6 100644 --- a/src/site/markdown/docs/introduction.md +++ b/src/site/markdown/docs/introduction.md @@ -37,4 +37,4 @@ The primary goals of the library are: 5. Small - the library is a small dependency to add. It has no transitive dependencies. This library grew out of a desire to create a utility that could be used to improve the code -generated by MyBatis Generator, but the library can be used on it's own with very little setup required. +generated by MyBatis Generator, but the library can be used on its own with very little setup required. diff --git a/src/site/markdown/docs/kotlinMyBatis3.md b/src/site/markdown/docs/kotlinMyBatis3.md index 03a5ea0c5..5287b8d1e 100644 --- a/src/site/markdown/docs/kotlinMyBatis3.md +++ b/src/site/markdown/docs/kotlinMyBatis3.md @@ -1,14 +1,14 @@ # Kotlin Support for MyBatis3 -MyBatis Dynamic SQL includes Kotlin extension methods that enable an SQL DSL for Kotlin. This is the recommended method of using the library in Kotlin with MyBatis3. +MyBatis Dynamic SQL includes Kotlin extensions that enable an SQL DSL for Kotlin. This is the recommended method of using the library in Kotlin with MyBatis3. -The standard usage patterns for MyBatis Dynamic SQL and MyBatis3 in Java must be modified somewhat for Kotlin. Kotlin interfaces can contain both abstract and non-abstract methods (somewhat similar to Java's default methods in an interface). But using these methods in Kotlin based mapper interfaces will cause a failure with MyBatis because of the underlying Kotlin implementation. +The standard usage patterns for MyBatis Dynamic SQL and MyBatis3 in Java must be modified somewhat for Kotlin. Kotlin interfaces can contain both abstract and non-abstract methods (somewhat similar to Java's default methods in an interface). Using these methods in Kotlin based mapper interfaces will cause a failure with MyBatis because of the underlying Kotlin implementation. This page will show our recommended pattern for using the MyBatis Dynamic SQL with Kotlin and MyBatis3. The code shown on this page is from the `src/test/kotlin/examples/kotlin/mybatis3/canonical` directory in this repository. That directory contains a complete example of using this library with Kotlin. All Kotlin support is available in two packages: * `org.mybatis.dynamic.sql.util.kotlin` - contains extension methods and utilities to enable an idiomatic Kotlin DSL for MyBatis Dynamic SQL. These objects can be used for clients using any execution target (i.e. MyBatis3 or Spring JDBC Templates) -* `org.mybatis.dynamic.sql.util.kotlin.mybatis3` - contains utlities specifically to simplify MyBatis3 based clients +* `org.mybatis.dynamic.sql.util.kotlin.mybatis3` - contains utilities specifically to simplify MyBatis3 based clients Using the support in these packages, it is possible to create reusable Kotlin classes, interfaces, and extension methods that mimic the code created by MyBatis Generator for Java - but code that is more idiomatic for Kotlin. @@ -32,7 +32,7 @@ object PersonDynamicSqlSupport { This object is a singleton containing the `SqlTable` and `SqlColumn` objects that map to the database table. ## Kotlin Mappers for MyBatis3 -If you create a Kotlin mapper interface that includes both abstract and non-abstract methods, MyBatis will be confused and throw errors. By default Kotlin does not create Java default methods in an interface. For this reason, Kotlin mapper interfaces should only contain the actual MyBatis mapper abstract interface methods. What would normally be coded as default or static methods in a mapper interface should be coded as extension methods in Kotlin. For example, a simple MyBatis mapper could be coded like this: +If you create a Kotlin mapper interface that includes both abstract and non-abstract methods, MyBatis will be confused and throw errors. By default, Kotlin does not create Java default methods in an interface. For this reason, Kotlin mapper interfaces should only contain the actual MyBatis mapper abstract interface methods. What would normally be coded as default or static methods in a mapper interface should be coded as extension methods in Kotlin. For example, a simple MyBatis mapper could be coded like this: ```kotlin @Mapper @@ -80,7 +80,7 @@ A count query is a specialized select - it returns a single column - typically a Count method support enables the creation of methods that execute a count query allowing a user to specify a where clause at runtime, but abstracting away all other details. -To use this support, we envision creating several methods - one standard mapper method, and other extension methods. The first method is the standard MyBatis Dynamic SQL method that will execute a select: +To use this support, we envision creating several methods - one standard mapper method, and other extension methods. The first method is the standard MyBatis Dynamic SQL method that will execute a select statement: ```kotlin @Mapper @@ -113,7 +113,7 @@ val rows = mapper.count { } ``` -There is also an extention method that can be used to count all rows in a table: +There is also an extension method that can be used to count all rows in a table: ```kotlin val rows = mapper.count { allRows() } @@ -188,7 +188,7 @@ fun PersonMapper.insert(record: PersonRecord) = map(addressId).toProperty("addressId") } -fun PersonMapper.insert(completer: GeneralInsertCompleter) = +fun PersonMapper.generalInsert(completer: GeneralInsertCompleter) = insertInto(this::generalInsert, Person, completer) fun PersonMapper.insertMultiple(vararg records: PersonRecord) = @@ -247,7 +247,7 @@ val rows = mapper.insertMultiple(record1, record2) Select method support enables the creation of methods that execute a query allowing a user to specify a where clause and/or an order by clause and/or pagination clauses at runtime, but abstracting away all other details. -To use this support, we envision creating several methods - two standard mapper methods, and other extension methods. The standard mapper methods are standard MyBatis Dynamic SQL methods that will execute a select: +To use this support, we envision creating several methods - two standard mapper methods, and other extension methods. The standard mapper methods are standard MyBatis Dynamic SQL methods that will execute a select statement: ```kotlin @Mapper @@ -307,7 +307,7 @@ val rows = mapper.select { } ``` -There is a utility methods that will select all rows in a table: +There is a utility method that will select all rows in a table: ```kotlin val rows = mapper.select { allRows() } diff --git a/src/site/markdown/docs/kotlinSpring.md b/src/site/markdown/docs/kotlinSpring.md index 87e418bbb..204168283 100644 --- a/src/site/markdown/docs/kotlinSpring.md +++ b/src/site/markdown/docs/kotlinSpring.md @@ -1,15 +1,21 @@ # Kotlin Support for Spring -MyBatis Dynamic SQL includes Kotlin extension methods that enable an SQL DSL for Kotlin. This is the recommended method of using the library in Kotlin with Spring JDBC template. +MyBatis Dynamic SQL includes Kotlin extensions that enable an SQL DSL for Kotlin. This is the recommended method of using the library in Kotlin with Spring JDBC template. It is possible to use the Java DSL with Kotlin without modification. The only difficulty with using the Java DSL directly is that the parameters for statements need to be formatted properly for Spring. This may involve the use of a `BeanPropertySqlParameterSource` or a `MapSqlParameterSource`. The Kotlin DSL hides all these details. This page will show our recommended pattern for using the MyBatis Dynamic SQL with Kotlin and Spring JDBC Template. The code shown on this page is from the `src/test/kotlin/examples/kotlin/spring/canonical` directory in this repository. That directory contains a complete example of using this library with Kotlin and Spring. All Kotlin support for Spring is available in two packages: * `org.mybatis.dynamic.sql.util.kotlin` - contains extension methods and utilities to enable an idiomatic Kotlin DSL for MyBatis Dynamic SQL. These objects can be used for clients using any execution target (i.e. MyBatis3 or Spring JDBC Templates) -* `org.mybatis.dynamic.sql.util.kotlin.spring` - contains utlities specifically to simplify integration with Spring JDBC Template +* `org.mybatis.dynamic.sql.util.kotlin.spring` - contains utilities specifically to simplify integration with Spring JDBC Template The Kotlin support for Spring is implemented as extension methods to `NamedParameterJdbcTemplate`. There are extension methods to support count, delete, insert, select, and update operations based on SQL generated by this library. For each operation, there are two different methods of executing SQL. With the first method you build the appropriate SQL provider object as a separate step before executing the SQL. The second method combines these two operations into a single step. We will illustrate both approaches below. +## General Note About the Kotlin DSL + +We take the customary approach to DSL building in Kotlin in that we attempt to create a somewhat natural feel for SQL, but not an exact replacement of SQL. The Kotlin DSL relies on the capabilities of the underlying Java DSL and does not replace it. This means that the Kotlin DSL does not add any capabilities that are not already present in the Java DSL. You can continue to use the underlying Java DSL at any time - it functions properly in Kotlin. One of the main features of the Kotlin DSL is that we move away from the method chaining paradigm of the Java DSL and move towards a more idiomatic Kotlin DSL based on lambdas and receiver objects. We think the Kotlin DSL feels more natural - certainly it is a more natural experience for Kotlin. + +One consequence of the more natural feel of the Kotlin DSL is that you are free to write unusual looking SQL. For example, you could write a SELECT statement with a WHERE clause after a UNION. Most of the time these unusual usages of the DSL will yield correct results. However, it would be best to use the DSL as shown below to avoid hard to diagnose problems. + ## Kotlin Dynamic SQL Support Objects Because Kotlin does not support static class members, we recommend a simpler pattern for creating the class containing the support objects. For example: @@ -20,7 +26,7 @@ object PersonDynamicSqlSupport { val firstName = column("first_name", JDBCType.VARCHAR) val lastName = column("last_name", JDBCType.VARCHAR) val birthDate = column("birth_date", JDBCType.DATE) - val employed = column("employed", JDBCType.VARCHAR) + val employed = column("employed", JDBCType.VARCHAR).withParameterTypeConverter(booleanToStringConverter) val occupation = column("occupation", JDBCType.VARCHAR) val addressId = column("address_id", JDBCType.INTEGER) } @@ -29,8 +35,25 @@ object PersonDynamicSqlSupport { This object is a singleton containing the `SqlTable` and `SqlColumn` objects that map to the database table. -**Important Note:** Spring JDBC template does not support type handlers, so column definitions in the support class should match the data types of the corresponding column. - +Note the use of a type converter on the `employed` column. This allows us to use the column as a Boolean in Kotlin, but store the values "Yes" or "No" on the database. The type converter looks like this: + +```kotlin +val booleanToStringConverter: (Boolean?) -> String = { it?.let { if (it) "Yes" else "No" } ?: "No" } +``` + +The type converter will be used on general insert statements, update statements, and where clauses. It is not used on insert statements that map insert fields to properties in a data class. So you will need to add properties to a data class to use in that case. In the examples below, you will see use of a data class property `employedAsString`. This can easily be implemented by reusing the converter function as shown below... + +```kotlin +data class PersonRecord( + ... + var employed: Boolean? = null, + ... +) { + val employedAsString: String + get() = booleanToStringConverter(employed) +} +``` + ## Count Method Support A count query is a specialized select - it returns a single column - typically a long - and supports joins and a where clause. @@ -60,7 +83,7 @@ The DSL for count methods looks like this: } ``` -Note the somewhat awkward method names `countColumn`, and `countDistinctColumn`. The methods are named this way to avoid a name collision with other methods in the `SqlBuilder`. This awkwardness can be avoided by using the one step method shown below. +Note the somewhat awkward method names `countColumn`, and `countDistinctColumn`. The methods are named this way to avoid a name collision with other methods in the `SqlBuilder`. This awkwardness can be avoided by using the one-step method shown below. These methods create a `SelectStatementProvider` that can be executed with an extension method for `NamedParameterJdbcTemplate` like this: @@ -69,7 +92,7 @@ These methods create a `SelectStatementProvider` that can be executed with an ex val rows = template.count(countStatement) // rows is a Long ``` -This is the two step execution process. This can be combined into a single step with code like the following: +This is the two-step execution process. This can be combined into a single step with code like the following: ```kotlin val rows = template.countFrom(Person) { @@ -85,7 +108,7 @@ This is the two step execution process. This can be combined into a single step } ``` -There is also an extention method that can be used to count all rows in a table: +There is also an extension method that can be used to count all rows in a table: ```kotlin val rows = template.countFrom(Person) { @@ -112,7 +135,7 @@ This code creates a `DeleteStatementProvider` that can be executed with an exten val rows = template.delete(deleteStatement) // rows is an Int ``` -This is the two step execution process. This can be combined into a single step with code like the following: +This is the two-step execution process. This can be combined into a single step with code like the following: ```kotlin val rows = template.deleteFrom(Person) { @@ -120,7 +143,7 @@ This is the two step execution process. This can be combined into a single step } ``` -There is also an extention method that can be used to count all rows in a table: +There is also an extension method that can be used to count all rows in a table: ```kotlin val rows = template.deleteFrom(Person) { @@ -135,14 +158,14 @@ Insert method support enables the creation of arbitrary insert statements given The DSL for insert methods looks like this: ```kotlin - val record = PersonRecord(100, "Joe", "Jones", Date(), "Yes", "Developer", 1) + val record = PersonRecord(100, "Joe", "Jones", Date(), true, "Developer", 1) val insertStatement = insert(record).into(Person) { // insertStatement is an InsertStatementProvider map(id).toProperty("id") map(firstName).toProperty("firstName") map(lastName).toProperty("lastName") map(birthDate).toProperty("birthDate") - map(employed).toProperty("employed") + map(employed).toProperty("employedAsString") map(occupation).toProperty("occupation") map(addressId).toProperty("addressId") } @@ -155,17 +178,24 @@ This code creates an `InsertStatementProvider` that can be executed with an exte val rows = template.insert(insertStatement) // rows is an Int ``` -This is the two step execution process. These steps can be combined into a single step with code like the following: +If you want to retrieve generated keys, you can use Spring's KeyHolder as follows: ```kotlin - val record = PersonRecord(100, "Joe", "Jones", Date(), "Yes", "Developer", 1) + val keyHolder = GeneratedKeyHolder() + val rows = template.insert(insertStatement, keyHolder) // rows is an Int +``` - val rows = template.insert(record, Person) { +This is the two-step execution process. These steps can be combined into a single step with code like the following: + +```kotlin + val record = PersonRecord(100, "Joe", "Jones", Date(), true, "Developer", 1) + + val rows = template.insert(record).into(Person) { map(id).toProperty("id") map(firstName).toProperty("firstName") map(lastName).toProperty("lastName") map(birthDate).toProperty("birthDate") - map(employed).toProperty("employed") + map(employed).toProperty("employedAsString") map(occupation).toPropertyWhenPresent("occupation", record::occupation) map(addressId).toProperty("addressId") } @@ -173,6 +203,25 @@ This is the two step execution process. These steps can be combined into a singl Note the use of the `toPropertyWhenPresent` mapping - this will only set the insert value if the value of the property is non null. Also note that you can use the mapping methods to map insert fields to nulls and constants if desired. +Using a KeyHolder with the single step looks like this: + +```kotlin + val keyHolder = GeneratedKeyHolder() + val record = PersonRecord(100, "Joe", "Jones", Date(), true, "Developer", 1) + + val rows = template.withKeyHolder(keyHolder) { + insert(record).into(Person) { + map(id).toProperty("id") + map(firstName).toProperty("firstName") + map(lastName).toProperty("lastName") + map(birthDate).toProperty("birthDate") + map(employed).toProperty("employedAsString") + map(occupation).toPropertyWhenPresent("occupation", record::occupation) + map(addressId).toProperty("addressId") + } + } +``` + ## General Insert Method Support General insert method support enables the creation of arbitrary insert statements and does not require the creation of a class matching the database row. @@ -185,7 +234,7 @@ The DSL for general insert methods looks like this: set(firstName).toValue("Joe") set(lastName).toValue("Jones") set(birthDate).toValue(Date()) - set(employed).toValue("Yes") + set(employed).toValue(true) set(occupation).toValue("Developer") set(addressId).toValue(1) } @@ -195,10 +244,17 @@ This code creates a `GeneralInsertStatementProvider` that can be executed with a ```kotlin val template: NamedParameterJdbcTemplate = getTemplate() // not shown - val rows = template.insert(insertStatement) // rows is an Int + val rows = template.generalInsert(insertStatement) // rows is an Int ``` -This is the two step execution process. These steps can be combined into a single step with code like the following: +If you want to retrieve generated keys, you can use Spring's KeyHolder as follows: + +```kotlin + val keyHolder = GeneratedKeyHolder() + val rows = template.generalInsert(insertStatement, keyHolder) // rows is an Int +``` + +This is the two-step execution process. These steps can be combined into a single step with code like the following: ```kotlin val myOccupation = "Developer" @@ -208,7 +264,7 @@ This is the two step execution process. These steps can be combined into a singl set(firstName).toValue("Joe") set(lastName).toValue("Jones") set(birthDate).toValue(Date()) - set(employed).toValue("Yes") + set(employed).toValue(true) set(occupation).toValueWhenPresent(myOccupation) set(addressId).toValue(1) } @@ -216,6 +272,130 @@ This is the two step execution process. These steps can be combined into a singl Note the use of the `toValueWhenPresent` mapping - this will only set the insert value if the value of the property is non null. Also note that you can use the mapping methods to map insert fields to nulls and constants if desired. +Using a KeyHolder with the single step looks like this: + +```kotlin + val keyHolder = GeneratedKeyHolder() + val myOccupation = "Developer" + + val rows = template.withKeyHolder(keyHolder) { + insertInto(Person) { + set(id).toValue(100) + set(firstName).toValue("Joe") + set(lastName).toValue("Jones") + set(birthDate).toValue(Date()) + set(employed).toValue(true) + set(occupation).toValueWhenPresent(myOccupation) + set(addressId).toValue(1) + } + } +``` + +## Multi-Row Insert Support +Multi-row inserts allow you to insert multiple rows into a table with a single insert statement. This is a convenient way to insert multiple rows, but there are some limitations. Multi-row inserts are not intended for large bulk inserts because it is possible to create insert statements that exceed the number of prepared statement parameters allowed in JDBC. For bulk inserts, please consider using a JDBC batch (see below). + +The two-step process for multi-row inserts looks like this: + +```kotlin + val record1 = PersonRecord(100, "Joe", LastName("Jones"), Date(), true, "Developer", 1) + val record2 = PersonRecord(101, "Sarah", LastName("Smith"), Date(), true, "Architect", 2) + + val insertStatement = insertMultiple(record1, record2).into(Person) {// insertStatement is a MultiRowInsertStatementProvider + map(id).toProperty("id") + map(firstName).toProperty("firstName") + map(lastName).toProperty("lastNameAsString") + map(birthDate).toProperty("birthDate") + map(employed).toProperty("employedAsString") + map(occupation).toProperty("occupation") + map(addressId).toProperty("addressId") + } + + val rows = template.insertMultiple(insertStatement) // rows is an Int +``` + +If you want to retrieve generated keys, you can use Spring's KeyHolder as follows: + +```kotlin + val keyHolder = GeneratedKeyHolder() + val rows = template.insertMultiple(insertStatement, keyHolder) // rows is an Int +``` + +This is the two-step execution process. These steps can be combined into a single step with code like the following: + +```kotlin + val record1 = PersonRecord(100, "Joe", LastName("Jones"), Date(), true, "Developer", 1) + val record2 = PersonRecord(101, "Sarah", LastName("Smith"), Date(), true, "Architect", 2) + + val rows = template.insertMultiple(record1, record2).into(Person) { + map(id).toProperty("id") + map(firstName).toProperty("firstName") + map(lastName).toProperty("lastNameAsString") + map(birthDate).toProperty("birthDate") + map(employed).toProperty("employedAsString") + map(occupation).toProperty("occupation") + map(addressId).toProperty("addressId") + } +``` + +Using a KeyHolder with the single step looks like this: + +```kotlin + val keyHolder = GeneratedKeyHolder() + val record1 = PersonRecord(100, "Joe", LastName("Jones"), Date(), true, "Developer", 1) + val record2 = PersonRecord(101, "Sarah", LastName("Smith"), Date(), true, "Architect", 2) + + val rows = template.withKeyHolder(keyHolder) { + insertMultiple(record1, record2).into(Person) { + map(id).toProperty("id") + map(firstName).toProperty("firstName") + map(lastName).toProperty("lastNameAsString") + map(birthDate).toProperty("birthDate") + map(employed).toProperty("employedAsString") + map(occupation).toProperty("occupation") + map(addressId).toProperty("addressId") + } + } +``` + +## Batch Insert Support +Batch inserts use Spring's support for JDBC batches - which is an efficient way for doing bulk inserts that does not have the limitations of multi-row insert. Spring does not support returning generated keys from a JDBC batch, but in all other aspects a JDBC batch will likely perform better for large record sets. + +The two-step process for batch inserts looks like this: + +```kotlin + val record1 = PersonRecord(100, "Joe", LastName("Jones"), Date(), true, "Developer", 1) + val record2 = PersonRecord(101, "Sarah", LastName("Smith"), Date(), true, "Architect", 2) + + val insertStatement = insertBatch(record1, record2).into(Person) { + map(id).toProperty("id") + map(firstName).toProperty("firstName") + map(lastName).toProperty("lastNameAsString") + map(birthDate).toProperty("birthDate") + map(employed).toProperty("employedAsString") + map(occupation).toProperty("occupation") + map(addressId).toProperty("addressId") + } + + val rows = template.insertBatch(insertStatement) // rows is an IntArray +``` + +This is the two-step execution process. These steps can be combined into a single step with code like the following: + +```kotlin + val record1 = PersonRecord(100, "Joe", LastName("Jones"), Date(), true, "Developer", 1) + val record2 = PersonRecord(101, "Sarah", LastName("Smith"), Date(), true, "Architect", 2) + + val rows = template.insertBatch(record1, record2).into(Person) { + map(id).toProperty("id") + map(firstName).toProperty("firstName") + map(lastName).toProperty("lastNameAsString") + map(birthDate).toProperty("birthDate") + map(employed).toProperty("employedAsString") + map(occupation).toProperty("occupation") + map(addressId).toProperty("addressId") + } +``` + ## Select Method Support Select method support enables the creation of methods that execute a query allowing a user to specify a where clause and/or an order by clause and/or pagination clauses at runtime, but abstracting away all other details. @@ -241,20 +421,20 @@ This code creates a `SelectStatementProvider` that can be executed with an exten ```kotlin val template: NamedParameterJdbcTemplate = getTemplate() // not shown val rows = template.selectList(selectStatement) { rs, _ -> // rows is a List of PersonRecord in this case - val record = PersonRecord() - record.id = rs.getInt(1) - record.firstName = rs.getString(2) - record.lastName = rs.getString(3) - record.birthDate = rs.getTimestamp(4) - record.employed = rs.getString(5) - record.occupation = rs.getString(6) - record.addressId = rs.getInt(7) - record + PersonRecord().apply { + id = rs.getInt(1) + firstName = rs.getString(2) + lastName = rs.getString(3) + birthDate = rs.getTimestamp(4) + employed = "Yes" == rs.getString(5) + occupation = rs.getString(6) + addressId = rs.getInt(7) + } } ``` Note that you must provide a row mapper to tell Spring JDBC how to create result objects. -This is the two step execution process. This can be combined into a single step with code like the following: +This is the two-step execution process. This can be combined into a single step with code like the following: ```kotlin val rows = template.select(id, firstName, lastName, birthDate, employed, occupation, addressId) @@ -266,40 +446,40 @@ This is the two step execution process. This can be combined into a single step orderBy(id) limit(3) }.withRowMapper { rs, _ -> - val record = PersonRecord() - record.id = rs.getInt(1) - record.firstName = rs.getString(2) - record.lastName = rs.getString(3) - record.birthDate = rs.getTimestamp(4) - record.employed = rs.getString(5) - record.occupation = rs.getString(6) - record.addressId = rs.getInt(7) - record + PersonRecord().apply { + id = rs.getInt(1) + firstName = rs.getString(2) + lastName = rs.getString(3) + birthDate = rs.getTimestamp(4) + employed = "Yes" == rs.getString(5) + occupation = rs.getString(6) + addressId = rs.getInt(7) + } } ``` -There are similar methods for selecing a single row, or executing a select distinct query. For example, you could implement a "select by primary key" method using code like this: +There are similar methods for selecting a single row, or executing a select distinct query. For example, you could implement a "select by primary key" method using code like this: ```kotlin val record = template.selectOne(id, firstName, lastName, birthDate, employed, occupation, addressId) .from(Person) { where(id, isEqualTo(key)) }.withRowMapper { rs, _ -> - val record = PersonRecord() - record.id = rs.getInt(1) - record.firstName = rs.getString(2) - record.lastName = rs.getString(3) - record.birthDate = rs.getTimestamp(4) - record.employed = rs.getString(5) - record.occupation = rs.getString(6) - record.addressId = rs.getInt(7) - record + PersonRecord().apply { + id = rs.getInt(1) + firstName = rs.getString(2) + lastName = rs.getString(3) + birthDate = rs.getTimestamp(4) + employed = "Yes" == rs.getString(5) + occupation = rs.getString(6) + addressId = rs.getInt(7) + } } ``` In this case, the data type for `record` would be `PersonRecord?` - a nullable value. -There is also an extention method that can be used to select all rows in a table: +There is also an extension method that can be used to select all rows in a table: ```kotlin val rows = template.select(id, firstName, lastName, birthDate, employed, occupation, addressId) @@ -307,15 +487,15 @@ There is also an extention method that can be used to select all rows in a table allRows() orderBy(id) }.withRowMapper { rs, _ -> - val record = PersonRecord() - record.id = rs.getInt(1) - record.firstName = rs.getString(2) - record.lastName = rs.getString(3) - record.birthDate = rs.getTimestamp(4) - record.employed = rs.getString(5) - record.occupation = rs.getString(6) - record.addressId = rs.getInt(7) - record + PersonRecord().apply { + id = rs.getInt(1) + firstName = rs.getString(2) + lastName = rs.getString(3) + birthDate = rs.getTimestamp(4) + employed = "Yes" == rs.getString(5) + occupation = rs.getString(6) + addressId = rs.getInt(7) + } } ``` @@ -341,7 +521,7 @@ This code creates an `UpdateStatementProvider` that can be executed with an exte val rows = template.update(updateStatement) // rows is an Int ``` -This is the two step execution process. This can be combined into a single step with code like the following: +This is the two-step execution process. This can be combined into a single step with code like the following: ```kotlin val rows = template.update(Person) { @@ -350,7 +530,7 @@ This is the two step execution process. This can be combined into a single step } ``` -There a many different set mappings the allow setting values to null, constants, etc. There is also a mapping that will only set the column value if the passed value is non null. +There a many set mappings that allow setting values to null, constants, etc. There is also a mapping that will only set the column value if the passed value is non null. If you wish to update all rows in a table, simply omit the where clause: diff --git a/src/site/markdown/docs/mybatis3.md b/src/site/markdown/docs/mybatis3.md index fb1dea165..bb87fd1ee 100644 --- a/src/site/markdown/docs/mybatis3.md +++ b/src/site/markdown/docs/mybatis3.md @@ -16,7 +16,7 @@ To use this support, we envision creating several methods on a MyBatis mapper in long count(SelectStatementProvider selectStatement); ``` -This is a standard method for MyBatis Dynamic SQL that executes a query and returns a `long`. The other methods will reuse this method and supply everything needed to build the select statement except the where clause. There are several varients of count queries that may be useful: +This is a standard method for MyBatis Dynamic SQL that executes a query and returns a `long`. The other methods will reuse this method and supply everything needed to build the select statement except the where clause. There are several variants of count queries that may be useful: 1. `count(*)` - counts the number of rows that match a where clause 1. `count(column)` - counts the number of non-null column values that match a where clause diff --git a/src/site/markdown/docs/select.md b/src/site/markdown/docs/select.md index 3696281c5..ef476480c 100644 --- a/src/site/markdown/docs/select.md +++ b/src/site/markdown/docs/select.md @@ -169,7 +169,7 @@ The XML element should look like this: ``` diff --git a/src/site/markdown/docs/spring.md b/src/site/markdown/docs/spring.md index 721608f4d..bcfc0d8a8 100644 --- a/src/site/markdown/docs/spring.md +++ b/src/site/markdown/docs/spring.md @@ -12,15 +12,44 @@ The SQL statement objects are created in exactly the same way as for MyBatis - o .render(RenderingStrategies.SPRING_NAMED_PARAMETER); ``` -## Limitations +The generated SQL statement providers are compatible with Spring's `NamedParameterJdbcTemplate` in all cases. The only challenge comes with presenting statement parameters to Spring in the correct manner. To make this easier, the library provides a utility class `org.mybatis.dynamic.sql.util.spring.NamedParameterJdbcTemplateExtensions` that executes statements properly in all cases and hides the complexity of rendering statements and formatting parameters. All the examples below will show usage both with and without the utility class. -MyBatis3 is a higher level abstraction over JDBC than Spring JDBC templates. While most functions in the library will work with Spring, there are some functions that do not work: +## Type Converters for Spring -1. Spring JDBC templates do not have anything equivalent to a type handler in MyBatis3. Therefore it is best to use data types that can be automatically understood by Spring -1. The multiple row insert statement *will not* render properly for Spring. However, batch inserts *will* render properly for Spring +Spring JDBC templates do not have the equivalent of a type handler in MyBatis3. This is generally not a problem in processing results because you can build type conversions into your row handler. If you were manually creating the parameter map that is used as input to a Spring template you could perform a type conversion there too. But when you use MyBatis Dynamic SQL, the parameters are generated by the library, so you do not have the opportunity to perform type conversions directly. + +To address this issue, the library provides a parameter type converter that can be used to perform a type conversion before parameters are placed in a parameter map. + +For example, suppose we want to use a `Boolean` in Java to represent the value of a flag, but in the database the corresponding field is a `CHAR` field that expects values "true" or "false". This can be accomplished by using a `ParameterTypeConverter`. First create the converter as follows: + +```java +public class TrueFalseParameterConverter implements ParameterTypeConverter { + @Override + public String convert(Boolean source) { + return source == null ? null : source ? "true" : "false"; + } +} +``` + +The type converter is compatible with Spring's existing Converter interface. Associate the type converter with a SqlColumn as follows: + +```java +... + public final SqlColumn employed = column("employed", JDBCType.VARCHAR) + .withParameterTypeConverter(new TrueFalseParameterConverter()); +... +``` + +MyBatis Dynamic SQL will now call the converter function before corresponding parameters are placed into the generated parameter map. The converter will be called in the following cases: + +1. With a general insert statement when using the `set(...).toValue(...)` or `set(...).toValueWhenPresent(...)` mappings +1. With an update statement when using the `set(...).equalTo(...)` or `set(...).equalToWhenPresent(...)` mappings +1. With where clauses in any statement type that contain conditions referencing the field ## Executing Select Statements -The Spring Named Parameter JDBC template expects an SQL statement with parameter markers in the Spring format, and a set of matched parameters. MyBatis Dynamic SQL will generate both. The parameters returned from the generated SQL statement can be wrapped in a Spring `MapSqlParameterSource`. Spring also expects you to provide a row mapper for creating the returned objects. The following code shows a complete example: +The Spring Named Parameter JDBC template expects an SQL statement with parameter markers in the Spring format, and a set of matched parameters. MyBatis Dynamic SQL will generate both. The parameters returned from the generated SQL statement can be wrapped in a Spring `MapSqlParameterSource`. Spring also expects you to provide a row mapper for creating the returned objects. + +The following code shows a complete example without the utility class: ```java NamedParameterJdbcTemplate template = getTemplate(); // not shown @@ -47,7 +76,62 @@ The Spring Named Parameter JDBC template expects an SQL statement with parameter }); ``` -## Executing General Insert Statements +The following code shows a complete example with the utility class: + +```java + NamedParameterJdbcTemplate template = getTemplate(); // not shown + NamedParameterJdbcTemplateExtensions extensions = new NamedParameterJdbcTemplateExtensions(template); + + Buildable selectStatement = select(id, firstName, lastName, fullName) + .from(generatedAlways) + .where(id, isGreaterThan(3)) + .orderBy(id.descending()); + + List records = extensions.selectList(selectStatement, + new RowMapper(){ + @Override + public GeneratedAlwaysRecord mapRow(ResultSet rs, int rowNum) throws SQLException { + GeneratedAlwaysRecord record = new GeneratedAlwaysRecord(); + record.setId(rs.getInt(1)); + record.setFirstName(rs.getString(2)); + record.setLastName(rs.getString(3)); + record.setFullName(rs.getString(4)); + return record; + } + }); +``` + +The utility class also includes a `selectOne` method that returns an `Optional`. An example is shown below: + +```java + NamedParameterJdbcTemplate template = getTemplate(); // not shown + NamedParameterJdbcTemplateExtensions extensions = new NamedParameterJdbcTemplateExtensions(template); + + Buildable selectStatement = select(id, firstName, lastName, fullName) + .from(generatedAlways) + .where(id, isEqualTo(3)); + + Optional record = extensions.selectOne(selectStatement, + new RowMapper(){ + @Override + public GeneratedAlwaysRecord mapRow(ResultSet rs, int rowNum) throws SQLException { + GeneratedAlwaysRecord record = new GeneratedAlwaysRecord(); + record.setId(rs.getInt(1)); + record.setFirstName(rs.getString(2)); + record.setLastName(rs.getString(3)); + record.setFullName(rs.getString(4)); + return record; + } + }); +``` + +## Executing Insert Statements + +The library generates several types of insert statements. See the [Insert Statements](insert.html) page for details. + +Spring supports retrieval of generated keys for many types of inserts. This library has support for generated key retrieval where it is supported by Spring. + +### Executing General Insert Statements General insert statements do not require a POJO object matching a table row. Following is a complete example: ```java @@ -82,7 +166,26 @@ If you want to retrieve generated keys for a general insert statement the steps String generatedKey = (String) keyHolder.getKeys().get("FULL_NAME"); ``` -## Executing Insert Record Statements +This can be simplified by using the utility class as follows: + +```java + NamedParameterJdbcTemplate template = getTemplate(); // not shown + NamedParameterJdbcTemplateExtensions extensions = new NamedParameterJdbcTemplateExtensions(template); + + Buildable insertStatement = insertInto(generatedAlways) + .set(id).toValue(100) + .set(firstName).toValue("Bob") + .set(lastName).toValue("Jones"); + + // no generated key retrieval + int rows = extensions.generalInsert(insertStatement); + + // retrieve generated keys + KeyHolder keyHolder = new GeneratedKeyHolder(); + int rows = extensions.generalInsert(insertStatement, keyHolder); +``` + +### Executing Single Record Insert Statements Insert record statements are a bit different - MyBatis Dynamic SQL generates a properly formatted SQL string for Spring, but instead of a map of parameters, the parameter mappings are created for the inserted record itself. So the parameters for the Spring template are created by a `BeanPropertySqlParameterSource`. Generated keys in Spring are supported with a `GeneratedKeyHolder`. The following is a complete example: ```java @@ -108,8 +211,101 @@ Insert record statements are a bit different - MyBatis Dynamic SQL generates a p String generatedKey = (String) keyHolder.getKeys().get("FULL_NAME"); ``` -## Executing Batch Inserts -Batch insert support in Spring is a bit different than batch support in MyBatis3 and Spring does not support returning generated keys from a batch insert. The following is a complete example of a batch insert (note the use of `SqlParameterSourceUtils` to create an array of parameter sources from an array of input records): +This can be simplified by using the utility class as follows: + +```java + NamedParameterJdbcTemplate template = getTemplate(); // not shown + NamedParameterJdbcTemplateExtensions extensions = new NamedParameterJdbcTemplateExtensions(template); + + GeneratedAlwaysRecord record = new GeneratedAlwaysRecord(); + record.setId(100); + record.setFirstName("Bob"); + record.setLastName("Jones"); + + Buildable> insertStatement = insert(record) + .into(generatedAlways) + .map(id).toProperty("id") + .map(firstName).toProperty("firstName") + .map(lastName).toProperty("lastName"); + + // no generated key retrieval + int rows = extensions.insert(insertStatement); + + // retrieve generated keys + KeyHolder keyHolder = new GeneratedKeyHolder(); + int rows = extensions.insert(insertStatement, keyHolder); +``` + +### Multi-Row Inserts +A multi-row insert is a single insert statement with multiple VALUES clauses. This can be a convenient way in insert a small number of records into a table with a single statement. Note however that a multi-row insert is not suitable for large bulk inserts as it is possible to exceed the limit of prepared statement parameters with a large number of records. For that use case, use a batch insert (see below). + +With multi-row insert statements MyBatis Dynamic SQL generates a properly formatted SQL string for Spring. Instead of a map of parameters, the multiple records are stored in the generated provider object and the parameter mappings are created for the generated provider itself. The parameters for the Spring template are created by a `BeanPropertySqlParameterSource`. Generated keys in Spring are supported with a `GeneratedKeyHolder`. The following is a complete example: + +```java + NamedParameterJdbcTemplate template = getTemplate(); // not shown + + List records = new ArrayList<>(); + GeneratedAlwaysRecord record = new GeneratedAlwaysRecord(); + record.setId(100); + record.setFirstName("Bob"); + record.setLastName("Jones"); + records.add(record); + + record = new GeneratedAlwaysRecord(); + record.setId(101); + record.setFirstName("Jim"); + record.setLastName("Smith"); + records.add(record); + + MultiRowInsertStatementProvider insertStatement = insertMultiple(records).into(generatedAlways) + .map(id).toProperty("id") + .map(firstName).toProperty("firstName") + .map(lastName).toProperty("lastName") + .build() + .render(RenderingStrategies.SPRING_NAMED_PARAMETER); + + SqlParameterSource parameterSource = new BeanPropertySqlParameterSource(insertStatement); + KeyHolder keyHolder = new GeneratedKeyHolder(); + + int rows = template.update(insertStatement.getInsertStatement(), parameterSource, keyHolder); + String firstGeneratedKey = (String) keyHolder.getKeyList().get(0).get("FULL_NAME"); + String secondGeneratedKey = (String) keyHolder.getKeyList().get(1).get("FULL_NAME"); +``` + +This can be simplified by using the utility class as follows: + +```java + NamedParameterJdbcTemplate template = getTemplate(); // not shown + NamedParameterJdbcTemplateExtensions extensions = new NamedParameterJdbcTemplateExtensions(template); + + List records = new ArrayList<>(); + GeneratedAlwaysRecord record = new GeneratedAlwaysRecord(); + record.setId(100); + record.setFirstName("Bob"); + record.setLastName("Jones"); + records.add(record); + + record = new GeneratedAlwaysRecord(); + record.setId(101); + record.setFirstName("Jim"); + record.setLastName("Smith"); + records.add(record); + + Buildable> insertStatement = insertMultiple(records).into(generatedAlways) + .map(id).toProperty("id") + .map(firstName).toProperty("firstName") + .map(lastName).toProperty("lastName"); + + // no generated key retrieval + int rows = extensions.insertMultiple(insertStatement); + + // retrieve generated keys + KeyHolder keyHolder = new GeneratedKeyHolder(); + int rows = extensions.insertMultiple(insertStatement, keyHolder); +``` + +### Executing Batch Inserts +A JDBC batch insert is an efficient way to perform a bulk insert. It does not have the limitations of a multi-row insert and may perform better too. Spring does not support returning generated keys from a batch insert. The following is a complete example of a batch insert (note the use of `SqlParameterSourceUtils` to create an array of parameter sources from an array of input records): ```java NamedParameterJdbcTemplate template = getTemplate(); // not shown @@ -140,8 +336,64 @@ Batch insert support in Spring is a bit different than batch support in MyBatis3 int[] updateCounts = template.batchUpdate(batchInsert.getInsertStatementSQL(), batch); ``` -## Executing Delete and Update Statements -Updates and deletes use the `MapSqlParameterSource` as with select statements, but use the `update` method in the template. For example: +This can be simplified by using the utility class as follows: + +```java + NamedParameterJdbcTemplate template = getTemplate(); // not shown + NamedParameterJdbcTemplateExtensions extensions = new NamedParameterJdbcTemplateExtensions(template); + + List records = new ArrayList<>(); + GeneratedAlwaysRecord record = new GeneratedAlwaysRecord(); + record.setId(100); + record.setFirstName("Bob"); + record.setLastName("Jones"); + records.add(record); + + record = new GeneratedAlwaysRecord(); + record.setId(101); + record.setFirstName("Jim"); + record.setLastName("Smith"); + records.add(record); + + Buildable> insertStatement = insertBatch(records) + .into(generatedAlways) + .map(id).toProperty("id") + .map(firstName).toProperty("firstName") + .map(lastName).toProperty("lastName"); + + int[] updateCounts = extensions.insertBatch(insertStatement); +``` + +## Executing Delete Statements +Delete statements use the `MapSqlParameterSource` as with select statements, but use the `update` method in the template. For example: + +```java + NamedParameterJdbcTemplate template = getTemplate(); // not shown + + DeleteStatementProvider deleteStatement = deleteFrom(generatedAlways) + .where(id, isLessThan(3)) + .build() + .render(RenderingStrategies.SPRING_NAMED_PARAMETER); + + SqlParameterSource parameterSource = new MapSqlParameterSource(deleteStatement.getParameters()); + + int rows = template.update(deleteStatement.getDeleteStatement(), parameterSource); +``` + +This can be simplified by using the utility class as follows: + +```java + NamedParameterJdbcTemplate template = getTemplate(); // not shown + NamedParameterJdbcTemplateExtensions extensions = new NamedParameterJdbcTemplateExtensions(template); + + Buildable deleteStatement = deleteFrom(generatedAlways) + .where(id, isLessThan(3)); + + int rows = extensions.delete(deleteStatement); +``` + +## Executing Update Statements +Update statements use the `MapSqlParameterSource` as with select statements, but use the `update` method in the template. For example: ```java NamedParameterJdbcTemplate template = getTemplate(); // not shown @@ -156,3 +408,16 @@ Updates and deletes use the `MapSqlParameterSource` as with select statements, b int rows = template.update(updateStatement.getUpdateStatement(), parameterSource); ``` + +This can be simplified by using the utility class as follows: + +```java + NamedParameterJdbcTemplate template = getTemplate(); // not shown + NamedParameterJdbcTemplateExtensions extensions = new NamedParameterJdbcTemplateExtensions(template); + + Buildable updateStatement = update(generatedAlways) + .set(firstName).equalToStringConstant("Rob") + .where(id, isIn(1, 5, 22)); + + int rows = extensions.update(updateStatement); +``` diff --git a/src/site/markdown/docs/springBatch.md b/src/site/markdown/docs/springBatch.md index 79b8988f7..4b1855a97 100644 --- a/src/site/markdown/docs/springBatch.md +++ b/src/site/markdown/docs/springBatch.md @@ -97,4 +97,4 @@ MyBatis mapper methods should be configured to use the specialized `@SelectProvi ## Complete Example -The unit tests for MyBatis Dynamic SQL include a complete examples 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/animal/data/AnimalDataTest.java b/src/test/java/examples/animal/data/AnimalDataTest.java index 112dbb7fe..ce07ec02d 100644 --- a/src/test/java/examples/animal/data/AnimalDataTest.java +++ b/src/test/java/examples/animal/data/AnimalDataTest.java @@ -1383,7 +1383,7 @@ record = new AnimalData(); record.setBodyWeight(22.5); records.add(record); - BatchInsert batchInsert = insert(records) + BatchInsert batchInsert = insertBatch(records) .into(animalData) .map(id).toProperty("id") .map(animalName).toNull() @@ -1433,7 +1433,7 @@ record = new AnimalData(); record.setBodyWeight(22.5); records.add(record); - BatchInsert batchInsert = insert(records) + BatchInsert batchInsert = insertBatch(records) .into(animalData) .map(id).toProperty("id") .map(animalName).toStringConstant("Old Fred") diff --git a/src/test/java/examples/custom_render/JsonTestDynamicSqlSupport.java b/src/test/java/examples/custom_render/JsonTestDynamicSqlSupport.java index cbaf0deee..926ceef0d 100644 --- a/src/test/java/examples/custom_render/JsonTestDynamicSqlSupport.java +++ b/src/test/java/examples/custom_render/JsonTestDynamicSqlSupport.java @@ -25,7 +25,7 @@ public class JsonTestDynamicSqlSupport { public static SqlColumn id = jsonTest.column("id", JDBCType.INTEGER); public static SqlColumn description = jsonTest.column("description", JDBCType.VARCHAR); public static SqlColumn info = jsonTest.column("info", JDBCType.VARCHAR) - .withRenderingStrategy(new JsonRenderingStrategy()); + .withRenderingStrategy(new JsonRenderingStrategy()); public static class JsonTest extends SqlTable { public JsonTest() { diff --git a/src/test/java/examples/generated/always/mybatis/GeneratedAlwaysAnnotatedMapperTest.java b/src/test/java/examples/generated/always/mybatis/GeneratedAlwaysAnnotatedMapperTest.java index 1eef6ad15..78c012975 100644 --- a/src/test/java/examples/generated/always/mybatis/GeneratedAlwaysAnnotatedMapperTest.java +++ b/src/test/java/examples/generated/always/mybatis/GeneratedAlwaysAnnotatedMapperTest.java @@ -121,7 +121,7 @@ void testInsert() { } @Test - void testBatchInsertWithList() { + void testDeprecatedBatchInsertWithList() { try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) { GeneratedAlwaysAnnotatedMapper mapper = session.getMapper(GeneratedAlwaysAnnotatedMapper.class); List records = getTestRecords(); @@ -134,7 +134,7 @@ void testBatchInsertWithList() { .build() .render(RenderingStrategies.MYBATIS3); - batchInsert.insertStatements().stream().forEach(mapper::insert); + batchInsert.insertStatements().forEach(mapper::insert); session.commit(); @@ -148,7 +148,7 @@ void testBatchInsertWithList() { } @Test - void testBatchInsertWithArray() { + void testDeprecatedBatchInsertWithArray() { try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) { GeneratedAlwaysAnnotatedMapper mapper = session.getMapper(GeneratedAlwaysAnnotatedMapper.class); @@ -170,7 +170,7 @@ void testBatchInsertWithArray() { .build() .render(RenderingStrategies.MYBATIS3); - batchInsert.insertStatements().stream().forEach(mapper::insert); + batchInsert.insertStatements().forEach(mapper::insert); session.commit(); @@ -180,7 +180,68 @@ void testBatchInsertWithArray() { ); } } - + + @Test + void testBatchInsertWithList() { + try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) { + GeneratedAlwaysAnnotatedMapper mapper = session.getMapper(GeneratedAlwaysAnnotatedMapper.class); + List records = getTestRecords(); + + BatchInsert batchInsert = insertBatch(records) + .into(generatedAlways) + .map(id).toProperty("id") + .map(firstName).toProperty("firstName") + .map(lastName).toProperty("lastName") + .build() + .render(RenderingStrategies.MYBATIS3); + + batchInsert.insertStatements().forEach(mapper::insert); + + session.commit(); + + assertAll( + () -> assertThat(records.get(0).getFullName()).isEqualTo("George Jetson"), + () -> assertThat(records.get(1).getFullName()).isEqualTo("Jane Jetson"), + () -> assertThat(records.get(2).getFullName()).isEqualTo("Judy Jetson"), + () -> assertThat(records.get(3).getFullName()).isEqualTo("Elroy Jetson") + ); + } + } + + @Test + void testBatchInsertWithArray() { + try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) { + GeneratedAlwaysAnnotatedMapper mapper = session.getMapper(GeneratedAlwaysAnnotatedMapper.class); + + GeneratedAlwaysRecord record1 = new GeneratedAlwaysRecord(); + record1.setId(1000); + record1.setFirstName("George"); + record1.setLastName("Jetson"); + + GeneratedAlwaysRecord record2 = new GeneratedAlwaysRecord(); + record2.setId(1001); + record2.setFirstName("Jane"); + record2.setLastName("Jetson"); + + BatchInsert batchInsert = insertBatch(record1, record2) + .into(generatedAlways) + .map(id).toProperty("id") + .map(firstName).toProperty("firstName") + .map(lastName).toProperty("lastName") + .build() + .render(RenderingStrategies.MYBATIS3); + + batchInsert.insertStatements().forEach(mapper::insert); + + session.commit(); + + assertAll( + () -> assertThat(record1.getFullName()).isEqualTo("George Jetson"), + () -> assertThat(record2.getFullName()).isEqualTo("Jane Jetson") + ); + } + } + @Test void testMultiInsertWithRawMyBatisAnnotations() { try (SqlSession session = sqlSessionFactory.openSession()) { diff --git a/src/test/java/examples/generated/always/mybatis/PersonMapperTest.java b/src/test/java/examples/generated/always/mybatis/PersonMapperTest.java index c7381c679..63f8ef3fd 100644 --- a/src/test/java/examples/generated/always/mybatis/PersonMapperTest.java +++ b/src/test/java/examples/generated/always/mybatis/PersonMapperTest.java @@ -150,7 +150,6 @@ void testInsertSelectWithMultipleRecords() { rows = mapper.insertSelect(insertSelectStatement, keys); assertThat(rows).isEqualTo(2); - System.out.println(keys); assertThat(keys.get(0).getKey()).isEqualTo(24); assertThat(keys.get(1).getKey()).isEqualTo(25); diff --git a/src/test/java/examples/generated/always/spring/SpringTest.java b/src/test/java/examples/generated/always/spring/SpringTest.java index ce3c672f7..7b3db7253 100644 --- a/src/test/java/examples/generated/always/spring/SpringTest.java +++ b/src/test/java/examples/generated/always/spring/SpringTest.java @@ -17,6 +17,7 @@ import static examples.generated.always.spring.GeneratedAlwaysDynamicSqlSupport.*; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; import static org.mybatis.dynamic.sql.SqlBuilder.*; import java.util.ArrayList; @@ -25,12 +26,18 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mybatis.dynamic.sql.delete.render.DeleteStatementProvider; +import org.mybatis.dynamic.sql.insert.BatchInsertModel; +import org.mybatis.dynamic.sql.insert.GeneralInsertModel; +import org.mybatis.dynamic.sql.insert.InsertModel; +import org.mybatis.dynamic.sql.insert.MultiRowInsertModel; import org.mybatis.dynamic.sql.insert.render.BatchInsert; import org.mybatis.dynamic.sql.insert.render.GeneralInsertStatementProvider; import org.mybatis.dynamic.sql.insert.render.InsertStatementProvider; 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.Buildable; +import org.mybatis.dynamic.sql.util.spring.NamedParameterJdbcTemplateExtensions; import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; @@ -145,6 +152,30 @@ void testInsert() { assertThat(generatedKey).isEqualTo("Bob Jones"); } + @Test + void testInsertWithExtensions() { + GeneratedAlwaysRecord record = new GeneratedAlwaysRecord(); + record.setId(100); + record.setFirstName("Bob"); + record.setLastName("Jones"); + + Buildable> insertStatement = insert(record) + .into(generatedAlways) + .map(id).toProperty("id") + .map(firstName).toProperty("firstName") + .map(lastName).toProperty("lastName"); + + NamedParameterJdbcTemplateExtensions extensions = new NamedParameterJdbcTemplateExtensions(template); + + KeyHolder keyHolder = new GeneratedKeyHolder(); + + int rows = extensions.insert(insertStatement, keyHolder); + String generatedKey = (String) keyHolder.getKeys().get("FULL_NAME"); + + assertThat(rows).isEqualTo(1); + assertThat(generatedKey).isEqualTo("Bob Jones"); + } + @Test void testGeneralInsert() { GeneralInsertStatementProvider insertStatement = insertInto(generatedAlways) @@ -178,6 +209,23 @@ void testGeneralInsertWithGeneratedKey() { assertThat(generatedKey).isEqualTo("Bob Jones"); } + @Test + void testGeneralInsertWithGeneratedKeyAndExtensions() { + Buildable insertStatement = insertInto(generatedAlways) + .set(id).toValue(100) + .set(firstName).toValue("Bob") + .set(lastName).toValue("Jones"); + + NamedParameterJdbcTemplateExtensions extensions = new NamedParameterJdbcTemplateExtensions(template); + KeyHolder keyHolder = new GeneratedKeyHolder(); + + int rows = extensions.generalInsert(insertStatement, keyHolder); + String generatedKey = (String) keyHolder.getKeys().get("FULL_NAME"); + + assertThat(rows).isEqualTo(1); + assertThat(generatedKey).isEqualTo("Bob Jones"); + } + @Test void testInsertBatch() { List records = new ArrayList<>(); @@ -193,9 +241,9 @@ record = new GeneratedAlwaysRecord(); record.setLastName("Smith"); records.add(record); - SqlParameterSource[] batch = SqlParameterSourceUtils.createBatch(records.toArray()); + SqlParameterSource[] batch = SqlParameterSourceUtils.createBatch(records); - BatchInsert batchInsert = insert(records) + BatchInsert batchInsert = insertBatch(records) .into(generatedAlways) .map(id).toProperty("id") .map(firstName).toProperty("firstName") @@ -210,6 +258,66 @@ record = new GeneratedAlwaysRecord(); assertThat(updateCounts[1]).isEqualTo(1); } + @Test + void testInsertBatchWithExtensions() { + NamedParameterJdbcTemplateExtensions extensions = new NamedParameterJdbcTemplateExtensions(template); + + List records = new ArrayList<>(); + GeneratedAlwaysRecord record = new GeneratedAlwaysRecord(); + record.setId(100); + record.setFirstName("Bob"); + record.setLastName("Jones"); + records.add(record); + + record = new GeneratedAlwaysRecord(); + record.setId(101); + record.setFirstName("Jim"); + record.setLastName("Smith"); + records.add(record); + + Buildable> insertStatement = insertBatch(records) + .into(generatedAlways) + .map(id).toProperty("id") + .map(firstName).toProperty("firstName") + .map(lastName).toProperty("lastName"); + + int[] updateCounts = extensions.insertBatch(insertStatement); + + assertThat(updateCounts).hasSize(2); + assertThat(updateCounts[0]).isEqualTo(1); + assertThat(updateCounts[1]).isEqualTo(1); + } + + @Test + void testMultiRowInsert() { + NamedParameterJdbcTemplateExtensions extensions = new NamedParameterJdbcTemplateExtensions(template); + KeyHolder keyHolder = new GeneratedKeyHolder(); + + List records = new ArrayList<>(); + GeneratedAlwaysRecord record = new GeneratedAlwaysRecord(); + record.setId(100); + record.setFirstName("Bob"); + record.setLastName("Jones"); + records.add(record); + + record = new GeneratedAlwaysRecord(); + record.setId(101); + record.setFirstName("Jim"); + record.setLastName("Smith"); + records.add(record); + + Buildable> insertStatement = insertMultiple(records).into(generatedAlways) + .map(id).toProperty("id") + .map(firstName).toProperty("firstName") + .map(lastName).toProperty("lastName"); + + int rows = extensions.insertMultiple(insertStatement, keyHolder); + + assertThat(rows).isEqualTo(2); + assertThat(keyHolder.getKeyList().get(0)).contains(entry("FULL_NAME", "Bob Jones")); + assertThat(keyHolder.getKeyList().get(1)).contains(entry("FULL_NAME", "Jim Smith")); + } + @Test void testUpdate() { UpdateStatementProvider updateStatement = update(generatedAlways) @@ -223,6 +331,5 @@ void testUpdate() { int rows = template.update(updateStatement.getUpdateStatement(), parameterSource); assertThat(rows).isEqualTo(2); - } } diff --git a/src/test/java/examples/simple/PersonMapper.java b/src/test/java/examples/simple/PersonMapper.java index 3a8af9cb3..62dd88730 100644 --- a/src/test/java/examples/simple/PersonMapper.java +++ b/src/test/java/examples/simple/PersonMapper.java @@ -118,8 +118,8 @@ default int deleteByPrimaryKey(Integer id_) { ); } - default int insert(UnaryOperator completer) { - return MyBatis3Utils.insert(this::generalInsert, person, completer); + default int generalInsert(UnaryOperator completer) { + return MyBatis3Utils.generalInsert(this::generalInsert, person, completer); } default int insert(PersonRecord record) { diff --git a/src/test/java/examples/simple/PersonMapperTest.java b/src/test/java/examples/simple/PersonMapperTest.java index 0a9cbf987..9028805db 100644 --- a/src/test/java/examples/simple/PersonMapperTest.java +++ b/src/test/java/examples/simple/PersonMapperTest.java @@ -216,7 +216,7 @@ void testInsert() { void testGeneralInsert() { try (SqlSession session = sqlSessionFactory.openSession()) { PersonMapper mapper = session.getMapper(PersonMapper.class); - int rows = mapper.insert(c -> + int rows = mapper.generalInsert(c -> c.set(id).toValue(100) .set(firstName).toValue("Joe") .set(lastName).toValue(LastName.of("Jones")) diff --git a/src/test/java/examples/spring/AddressDynamicSqlSupport.java b/src/test/java/examples/spring/AddressDynamicSqlSupport.java new file mode 100644 index 000000000..e633ee7e4 --- /dev/null +++ b/src/test/java/examples/spring/AddressDynamicSqlSupport.java @@ -0,0 +1,40 @@ +/** + * Copyright 2016-2020 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package examples.spring; + +import java.sql.JDBCType; + +import org.mybatis.dynamic.sql.SqlColumn; +import org.mybatis.dynamic.sql.SqlTable; + +public final class AddressDynamicSqlSupport { + public static final Address address = new Address(); + public static final SqlColumn id = address.id; + public static final SqlColumn streetAddress = address.streetAddress; + public static final SqlColumn city = address.city; + public static final SqlColumn state = address.state; + + public static final class Address extends SqlTable { + public final SqlColumn id = column("address_id", JDBCType.INTEGER); + public final SqlColumn streetAddress = column("street_address", JDBCType.VARCHAR); + public final SqlColumn city = column("city", JDBCType.VARCHAR); + public final SqlColumn state = column("state", JDBCType.VARCHAR); + + public Address() { + super("Address"); + } + } +} diff --git a/src/test/java/examples/spring/AddressRecord.java b/src/test/java/examples/spring/AddressRecord.java new file mode 100644 index 000000000..e0fb9a7c7 --- /dev/null +++ b/src/test/java/examples/spring/AddressRecord.java @@ -0,0 +1,55 @@ +/** + * Copyright 2016-2020 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package examples.spring; + +public class AddressRecord { + private Integer id; + private String streetAddress; + private String city; + private String state; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getStreetAddress() { + return streetAddress; + } + + public void setStreetAddress(String streetAddress) { + this.streetAddress = streetAddress; + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } +} diff --git a/src/test/java/examples/spring/LastName.java b/src/test/java/examples/spring/LastName.java new file mode 100644 index 000000000..8b3d993f7 --- /dev/null +++ b/src/test/java/examples/spring/LastName.java @@ -0,0 +1,59 @@ +/** + * Copyright 2016-2020 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package examples.spring; + +public class LastName { + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public static LastName of(String name) { + LastName lastName = new LastName(); + lastName.setName(name); + return lastName; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((name == null) ? 0 : name.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + LastName other = (LastName) obj; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + return true; + } +} diff --git a/src/test/kotlin/examples/kotlin/spring/canonical/PersonRecord.kt b/src/test/java/examples/spring/LastNameParameterConverter.java similarity index 57% rename from src/test/kotlin/examples/kotlin/spring/canonical/PersonRecord.kt rename to src/test/java/examples/spring/LastNameParameterConverter.java index e0052363e..aa4c24cf2 100644 --- a/src/test/kotlin/examples/kotlin/spring/canonical/PersonRecord.kt +++ b/src/test/java/examples/spring/LastNameParameterConverter.java @@ -1,5 +1,5 @@ /** - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2020 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. @@ -13,16 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package examples.kotlin.spring.canonical +package examples.spring; -import java.util.* +import org.mybatis.dynamic.sql.ParameterTypeConverter; +import org.springframework.core.convert.converter.Converter; -data class PersonRecord( - var id: Int? = null, - var firstName: String? = null, - var lastName: String? = null, - var birthDate: Date? = null, - var employed: String? = null, - var occupation: String? = null, - var addressId: Int? = null -) +public class LastNameParameterConverter implements ParameterTypeConverter, Converter { + @Override + public String convert(LastName source) { + return source == null ? null : source.getName(); + } +} diff --git a/src/test/java/examples/spring/PersonDynamicSqlSupport.java b/src/test/java/examples/spring/PersonDynamicSqlSupport.java new file mode 100644 index 000000000..8eebfff8c --- /dev/null +++ b/src/test/java/examples/spring/PersonDynamicSqlSupport.java @@ -0,0 +1,49 @@ +/** + * Copyright 2016-2020 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package examples.spring; + +import java.sql.JDBCType; +import java.util.Date; + +import org.mybatis.dynamic.sql.SqlColumn; +import org.mybatis.dynamic.sql.SqlTable; + +public final class PersonDynamicSqlSupport { + public static final Person person = new Person(); + public static final SqlColumn id = person.id; + public static final SqlColumn firstName = person.firstName; + public static final SqlColumn lastName = person.lastName; + public static final SqlColumn birthDate = person.birthDate; + public static final SqlColumn employed = person.employed; + public static final SqlColumn occupation = person.occupation; + public static final SqlColumn addressId = person.addressId; + + public static final class Person extends SqlTable { + public final SqlColumn id = column("id", JDBCType.INTEGER); + public final SqlColumn firstName = column("first_name", JDBCType.VARCHAR); + public final SqlColumn lastName = column("last_name", JDBCType.VARCHAR) + .withParameterTypeConverter(new LastNameParameterConverter()); + public final SqlColumn birthDate = column("birth_date", JDBCType.DATE); + public final SqlColumn employed = column("employed", JDBCType.VARCHAR) + .withParameterTypeConverter(new YesNoParameterConverter()); + public final SqlColumn occupation = column("occupation", JDBCType.VARCHAR); + public final SqlColumn addressId = column("address_id", JDBCType.INTEGER); + + public Person() { + super("Person"); + } + } +} diff --git a/src/test/java/examples/spring/PersonRecord.java b/src/test/java/examples/spring/PersonRecord.java new file mode 100644 index 000000000..6d0d7bc10 --- /dev/null +++ b/src/test/java/examples/spring/PersonRecord.java @@ -0,0 +1,92 @@ +/** + * Copyright 2016-2020 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package examples.spring; + +import java.util.Date; + +public class PersonRecord { + private Integer id; + private String firstName; + private LastName lastName; + private Date birthDate; + private Boolean employed; + private String occupation; + private Integer addressId; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public LastName getLastName() { + return lastName; + } + + public String getLastNameAsString() { + return lastName == null ? null : lastName.getName(); + } + + public void setLastName(LastName lastName) { + this.lastName = lastName; + } + + public Date getBirthDate() { + return birthDate; + } + + public void setBirthDate(Date birthDate) { + this.birthDate = birthDate; + } + + public String getOccupation() { + return occupation; + } + + public void setOccupation(String occupation) { + this.occupation = occupation; + } + + public Boolean getEmployed() { + return employed; + } + + public String getEmployedAsString() { + return employed == null ? "No" : employed ? "Yes" : "No"; + } + + public void setEmployed(Boolean employed) { + this.employed = employed; + } + + public Integer getAddressId() { + return addressId; + } + + public void setAddressId(Integer addressId) { + this.addressId = addressId; + } +} diff --git a/src/test/java/examples/spring/PersonTemplateTest.java b/src/test/java/examples/spring/PersonTemplateTest.java new file mode 100644 index 000000000..4b8e9e386 --- /dev/null +++ b/src/test/java/examples/spring/PersonTemplateTest.java @@ -0,0 +1,785 @@ +/** + * Copyright 2016-2020 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package examples.spring; + +import static examples.spring.AddressDynamicSqlSupport.address; +import static examples.spring.PersonDynamicSqlSupport.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mybatis.dynamic.sql.SqlBuilder.*; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mybatis.dynamic.sql.delete.DeleteModel; +import org.mybatis.dynamic.sql.insert.BatchInsertModel; +import org.mybatis.dynamic.sql.insert.GeneralInsertModel; +import org.mybatis.dynamic.sql.insert.InsertModel; +import org.mybatis.dynamic.sql.insert.MultiRowInsertModel; +import org.mybatis.dynamic.sql.select.SelectModel; +import org.mybatis.dynamic.sql.update.UpdateModel; +import org.mybatis.dynamic.sql.util.Buildable; +import org.mybatis.dynamic.sql.util.spring.NamedParameterJdbcTemplateExtensions; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; + +class PersonTemplateTest { + + private NamedParameterJdbcTemplateExtensions template; + + @BeforeEach + void setup() throws Exception { + EmbeddedDatabase db = new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.HSQL) + .generateUniqueName(true) + .addScript("classpath:/examples/simple/CreateSimpleDB.sql") + .build(); + template = new NamedParameterJdbcTemplateExtensions(new NamedParameterJdbcTemplate(db)); + } + + @Test + void testSelect() { + Buildable selectStatement = select(id, firstName, lastName, birthDate, employed, occupation, addressId) + .from(person) + .where(id, isEqualTo(1)) + .or(occupation, isNull()); + + List rows = template.selectList(selectStatement, personRowMapper); + + assertThat(rows).hasSize(3); + } + + @Test + void testSelectAll() { + Buildable selectStatement = select(id, firstName, lastName, birthDate, employed, occupation, addressId) + .from(person); + + List rows = template.selectList(selectStatement, personRowMapper); + + assertThat(rows).hasSize(6); + assertThat(rows.get(0).getId()).isEqualTo(1); + assertThat(rows.get(5).getId()).isEqualTo(6); + + } + + @Test + void testSelectAllOrdered() { + Buildable selectStatement = select(id, firstName, lastName, birthDate, employed, occupation, addressId) + .from(person) + .orderBy(lastName.descending(), firstName.descending()); + + List rows = template.selectList(selectStatement, personRowMapper); + + assertThat(rows).hasSize(6); + assertThat(rows.get(0).getId()).isEqualTo(5); + assertThat(rows.get(5).getId()).isEqualTo(1); + + } + + @Test + void testSelectDistinct() { + Buildable selectStatement = selectDistinct(id, firstName, lastName, birthDate, employed, occupation, addressId) + .from(person) + .where(id, isGreaterThan(1)) + .or(occupation, isNull()); + + List rows = template.selectList(selectStatement, personRowMapper); + + assertThat(rows).hasSize(5); + } + + @Test + void testSelectWithUnion() { + Buildable selectStatement = select(id, firstName, lastName, birthDate, employed, occupation, addressId) + .from(person) + .where(id, isEqualTo(1)) + .union() + .select(id, firstName, lastName, birthDate, employed, occupation, addressId) + .from(person) + .where(id, isEqualTo(2)) + .union() + .select(id, firstName, lastName, birthDate, employed, occupation, addressId) + .from(person) + .where(id, isEqualTo(2)); + + + List rows = template.selectList(selectStatement, personRowMapper); + + assertThat(rows).hasSize(2); + } + + @Test + void testSelectWithUnionAll() { + Buildable selectStatement = select(id, firstName, lastName, birthDate, employed, occupation, addressId) + .from(person) + .where(id, isEqualTo(1)) + .union() + .select(id, firstName, lastName, birthDate, employed, occupation, addressId) + .from(person) + .where(id, isEqualTo(2)) + .unionAll() + .select(id, firstName, lastName, birthDate, employed, occupation, addressId) + .from(person) + .where(id, isEqualTo(2)); + + + List rows = template.selectList(selectStatement, personRowMapper); + + assertThat(rows).hasSize(3); + } + + @Test + void testSelectWithTypeHandler() { + Buildable selectStatement = select(id, firstName, lastName, birthDate, employed, occupation, addressId) + .from(person) + .where(employed, isEqualTo(false)) + .orderBy(id); + + List rows = template.selectList(selectStatement, personRowMapper); + + assertThat(rows).hasSize(2); + assertThat(rows.get(0).getId()).isEqualTo(3); + assertThat(rows.get(1).getId()).isEqualTo(6); + } + + @Test + void testSelectBetweenWithTypeHandler() { + Buildable selectStatement = select(id, firstName, lastName, birthDate, employed, occupation, addressId) + .from(person) + .where(lastName, isBetween(LastName.of("Adams")).and(LastName.of("Jones"))) + .orderBy(id); + + List rows = template.selectList(selectStatement, personRowMapper); + + assertThat(rows).hasSize(3); + assertThat(rows.get(0).getId()).isEqualTo(1); + assertThat(rows.get(1).getId()).isEqualTo(2); + } + + @Test + void testSelectListWithTypeHandler() { + Buildable selectStatement = select(id, firstName, lastName, birthDate, employed, occupation, addressId) + .from(person) + .where(lastName, isIn(LastName.of("Flintstone"), LastName.of("Rubble"))) + .orderBy(id); + + List rows = template.selectList(selectStatement, personRowMapper); + + assertThat(rows).hasSize(6); + assertThat(rows.get(0).getId()).isEqualTo(1); + assertThat(rows.get(1).getId()).isEqualTo(2); + } + + @Test + void testSelectByPrimaryKeyWithMissingRecord() { + Buildable selectStatement = select(id, firstName, lastName, birthDate, employed, occupation, addressId) + .from(person) + .where(id, isEqualTo(300)); + + Optional record = template.selectOne(selectStatement, personRowMapper); + + assertThat(record).isNotPresent(); + } + + @Test + void testFirstNameIn() { + Buildable selectStatement = select(id, firstName, lastName, birthDate, employed, occupation, addressId) + .from(person) + .where(firstName, isIn("Fred", "Barney")); + + List rows = template.selectList(selectStatement, personRowMapper); + + assertThat(rows).hasSize(2); + assertThat(rows.get(0).getLastName().getName()).isEqualTo("Flintstone"); + assertThat(rows.get(1).getLastName().getName()).isEqualTo("Rubble"); + } + + @Test + void testDelete() { + Buildable deleteStatement = deleteFrom(person) + .where(occupation, isNull()); + + int rows = template.delete(deleteStatement); + + assertThat(rows).isEqualTo(2); + } + + @Test + void testDeleteAll() { + Buildable deleteStatement = deleteFrom(person); + + int rows = template.delete(deleteStatement); + + assertThat(rows).isEqualTo(6); + } + + @Test + void testDeleteByPrimaryKey() { + Buildable deleteStatement = deleteFrom(person) + .where(id, isEqualTo(2)); + + int rows = template.delete(deleteStatement); + + assertThat(rows).isEqualTo(1); + } + + @Test + void testInsert() { + PersonRecord record = new PersonRecord(); + record.setId(100); + record.setFirstName("Joe"); + record.setLastName(LastName.of("Jones")); + record.setBirthDate(new Date()); + record.setEmployed(true); + record.setOccupation("Developer"); + record.setAddressId(1); + + Buildable> insertStatement = insert(record).into(person) + .map(id).toProperty("id") + .map(firstName).toProperty("firstName") + .map(lastName).toProperty("lastNameAsString") + .map(birthDate).toProperty("birthDate") + .map(employed).toProperty("employedAsString") + .map(occupation).toProperty("occupation") + .map(addressId).toProperty("addressId"); + + int rows = template.insert(insertStatement); + + assertThat(rows).isEqualTo(1); + } + + @Test + void testGeneralInsert() { + Buildable insertStatement = insertInto(person) + .set(id).toValue(100) + .set(firstName).toValue("Joe") + .set(lastName).toValue(LastName.of("Jones")) + .set(birthDate).toValue(new Date()) + .set(employed).toValue(true) + .set(occupation).toValue("Developer") + .set(addressId).toValue(1); + + int rows = template.generalInsert(insertStatement); + + assertThat(rows).isEqualTo(1); + } + + @Test + void testInsertMultiple() { + + List records = new ArrayList<>(); + + PersonRecord record = new PersonRecord(); + record.setId(100); + record.setFirstName("Joe"); + record.setLastName(LastName.of("Jones")); + record.setBirthDate(new Date()); + record.setEmployed(true); + record.setOccupation("Developer"); + record.setAddressId(1); + records.add(record); + + record = new PersonRecord(); + record.setId(101); + record.setFirstName("Sarah"); + record.setLastName(LastName.of("Smith")); + record.setBirthDate(new Date()); + record.setEmployed(true); + record.setOccupation("Architect"); + record.setAddressId(2); + records.add(record); + + Buildable> insertStatement = insertMultiple(records).into(person) + .map(id).toProperty("id") + .map(firstName).toProperty("firstName") + .map(lastName).toProperty("lastNameAsString") + .map(birthDate).toProperty("birthDate") + .map(employed).toProperty("employedAsString") + .map(occupation).toProperty("occupation") + .map(addressId).toProperty("addressId"); + + int rows = template.insertMultiple(insertStatement); + + assertThat(rows).isEqualTo(2); + } + + @Test + void testInsertBatch() { + + List records = new ArrayList<>(); + + PersonRecord record = new PersonRecord(); + record.setId(100); + record.setFirstName("Joe"); + record.setLastName(LastName.of("Jones")); + record.setBirthDate(new Date()); + record.setEmployed(true); + record.setOccupation("Developer"); + record.setAddressId(1); + records.add(record); + + record = new PersonRecord(); + record.setId(101); + record.setFirstName("Sarah"); + record.setLastName(LastName.of("Smith")); + record.setBirthDate(new Date()); + record.setEmployed(true); + record.setOccupation("Architect"); + record.setAddressId(2); + records.add(record); + + Buildable> insertStatement = insertBatch(records).into(person) + .map(id).toProperty("id") + .map(firstName).toProperty("firstName") + .map(lastName).toProperty("lastNameAsString") + .map(birthDate).toProperty("birthDate") + .map(employed).toProperty("employedAsString") + .map(occupation).toProperty("occupation") + .map(addressId).toProperty("addressId"); + + int[] rows = template.insertBatch(insertStatement); + + assertThat(rows).hasSize(2); + assertThat(rows[0]).isEqualTo(1); + assertThat(rows[1]).isEqualTo(1); + } + + @Test + void testInsertSelective() { + PersonRecord record = new PersonRecord(); + record.setId(100); + record.setFirstName("Joe"); + record.setLastName(LastName.of("Jones")); + record.setBirthDate(new Date()); + record.setEmployed(false); + record.setAddressId(1); + + Buildable> insertStatement = insert(record).into(person) + .map(id).toPropertyWhenPresent("id", record::getId) + .map(firstName).toPropertyWhenPresent("firstName", record::getFirstName) + .map(lastName).toPropertyWhenPresent("lastNameAsString", record::getLastNameAsString) + .map(birthDate).toPropertyWhenPresent("birthDate", record::getBirthDate) + .map(employed).toPropertyWhenPresent("employedAsString", record::getEmployedAsString) + .map(occupation).toPropertyWhenPresent("occupation", record::getOccupation) + .map(addressId).toPropertyWhenPresent("addressId", record::getAddressId); + + int rows = template.insert(insertStatement); + + assertThat(rows).isEqualTo(1); + } + + @Test + void testUpdateByPrimaryKey() { + + Buildable insertStatement = insertInto(person) + .set(id).toValue(100) + .set(firstName).toValue("Joe") + .set(lastName).toValue(LastName.of("Jones")) + .set(birthDate).toValue(new Date()) + .set(employed).toValue(true) + .set(occupation).toValue("Developer") + .set(addressId).toValue(1); + + int rows = template.generalInsert(insertStatement); + assertThat(rows).isEqualTo(1); + + Buildable updateStatement = update(person) + .set(occupation).equalTo("Programmer") + .where(id, isEqualTo(100)); + + rows = template.update(updateStatement); + assertThat(rows).isEqualTo(1); + + Buildable selectStatement = select(id, firstName, lastName, birthDate, employed, occupation, addressId) + .from(person) + .where(id, isEqualTo(100)); + Optional newRecord = template.selectOne(selectStatement, personRowMapper); + assertThat(newRecord).isPresent(); + assertThat(newRecord.get().getOccupation()).isEqualTo("Programmer"); + } + + @Test + void testUpdateByPrimaryKeyWithTypeHandler() { + + Buildable insertStatement = insertInto(person) + .set(id).toValue(100) + .set(firstName).toValue("Joe") + .set(lastName).toValue(LastName.of("Jones")) + .set(birthDate).toValue(new Date()) + .set(employed).toValue(true) + .set(occupation).toValue("Developer") + .set(addressId).toValue(1); + + int rows = template.generalInsert(insertStatement); + assertThat(rows).isEqualTo(1); + + Buildable updateStatement = update(person) + .set(lastName).equalTo(LastName.of("Smith")) + .where(id, isEqualTo(100)); + + rows = template.update(updateStatement); + assertThat(rows).isEqualTo(1); + + Buildable selectStatement = select(id, firstName, lastName, birthDate, employed, occupation, addressId) + .from(person) + .where(id, isEqualTo(100)); + Optional newRecord = template.selectOne(selectStatement, personRowMapper); + assertThat(newRecord).isPresent(); + assertThat(newRecord.get().getLastName().getName()).isEqualTo("Smith"); + } + + @Test + void testUpdateByPrimaryKeySelective() { + Buildable insertStatement = insertInto(person) + .set(id).toValue(100) + .set(firstName).toValue("Joe") + .set(lastName).toValue(LastName.of("Jones")) + .set(birthDate).toValue(new Date()) + .set(employed).toValue(true) + .set(occupation).toValue("Developer") + .set(addressId).toValue(1); + + int rows = template.generalInsert(insertStatement); + assertThat(rows).isEqualTo(1); + + PersonRecord updateRecord = new PersonRecord(); + updateRecord.setId(100); + updateRecord.setOccupation("Programmer"); + + Buildable updateStatement = update(person) + .set(firstName).equalToWhenPresent(updateRecord::getFirstName) + .set(lastName).equalToWhenPresent(updateRecord::getLastName) + .set(birthDate).equalToWhenPresent(updateRecord::getBirthDate) + .set(employed).equalToWhenPresent(updateRecord::getEmployed) + .set(occupation).equalToWhenPresent(updateRecord::getOccupation) + .set(addressId).equalToWhenPresent(updateRecord::getAddressId) + .where(id, isEqualTo(updateRecord::getId)); + + rows = template.update(updateStatement); + assertThat(rows).isEqualTo(1); + + Buildable selectStatement = select(id, firstName, lastName, birthDate, employed, occupation, addressId) + .from(person) + .where(id, isEqualTo(100)); + Optional newRecord = template.selectOne(selectStatement, personRowMapper); + assertThat(newRecord).isPresent(); + assertThat(newRecord.get().getOccupation()).isEqualTo("Programmer"); + assertThat(newRecord.get().getFirstName()).isEqualTo("Joe"); + } + + @Test + void testUpdate() { + PersonRecord record = new PersonRecord(); + record.setId(100); + record.setFirstName("Joe"); + record.setLastName(LastName.of("Jones")); + record.setBirthDate(new Date()); + record.setEmployed(true); + record.setOccupation("Developer"); + record.setAddressId(1); + + Buildable> insertStatement = insert(record).into(person) + .map(id).toProperty("id") + .map(firstName).toProperty("firstName") + .map(lastName).toProperty("lastNameAsString") + .map(birthDate).toProperty("birthDate") + .map(employed).toProperty("employedAsString") + .map(occupation).toProperty("occupation") + .map(addressId).toProperty("addressId"); + + int rows = template.insert(insertStatement); + assertThat(rows).isEqualTo(1); + + record.setOccupation("Programmer"); + + Buildable updateStatement = update(person) + .set(firstName).equalTo(record::getFirstName) + .set(lastName).equalTo(record::getLastName) + .set(birthDate).equalTo(record::getBirthDate) + .set(employed).equalTo(record::getEmployed) + .set(occupation).equalTo(record::getOccupation) + .set(addressId).equalTo(record::getAddressId) + .where(id, isEqualTo(record::getId)); + + rows = template.update(updateStatement); + assertThat(rows).isEqualTo(1); + + Buildable selectStatement = select(id, firstName, lastName, birthDate, employed, occupation, addressId) + .from(person) + .where(id, isEqualTo(100)); + Optional newRecord = template.selectOne(selectStatement, personRowMapper); + assertThat(newRecord).isPresent(); + assertThat(newRecord.get().getOccupation()).isEqualTo("Programmer"); + assertThat(newRecord.get().getFirstName()).isEqualTo("Joe"); + } + + @Test + void testUpdateOneField() { + Buildable insertStatement = insertInto(person) + .set(id).toValue(100) + .set(firstName).toValue("Joe") + .set(lastName).toValue(LastName.of("Jones")) + .set(birthDate).toValue(new Date()) + .set(employed).toValue(true) + .set(occupation).toValue("Developer") + .set(addressId).toValue(1); + + int rows = template.generalInsert(insertStatement); + assertThat(rows).isEqualTo(1); + + Buildable updateStatement = update(person) + .set(occupation).equalTo("Programmer") + .where(id, isEqualTo(100)); + rows = template.update(updateStatement); + assertThat(rows).isEqualTo(1); + + Buildable selectStatement = select(id, firstName, lastName, birthDate, employed, occupation, addressId) + .from(person) + .where(id, isEqualTo(100)); + Optional newRecord = template.selectOne(selectStatement, personRowMapper); + assertThat(newRecord).isPresent(); + assertThat(newRecord.get().getOccupation()).isEqualTo("Programmer"); + } + + @Test + void testUpdateAll() { + Buildable insertStatement = insertInto(person) + .set(id).toValue(100) + .set(firstName).toValue("Joe") + .set(lastName).toValue(LastName.of("Jones")) + .set(birthDate).toValue(new Date()) + .set(employed).toValue(true) + .set(occupation).toValue("Developer") + .set(addressId).toValue(1); + + int rows = template.generalInsert(insertStatement); + assertThat(rows).isEqualTo(1); + + Buildable updateStatement = update(person) + .set(occupation).equalTo("Programmer"); + + rows = template.update(updateStatement); + assertThat(rows).isEqualTo(7); + + Buildable selectStatement = select(id, firstName, lastName, birthDate, employed, occupation, addressId) + .from(person) + .where(id, isEqualTo(100)); + + Optional newRecord = template.selectOne(selectStatement, personRowMapper); + assertThat(newRecord).isPresent(); + assertThat(newRecord.get().getOccupation()).isEqualTo("Programmer"); + } + + @Test + void testCount() { + Buildable countStatement = countFrom(person) + .where(occupation, isNull()); + + long rows = template.count(countStatement); + assertThat(rows).isEqualTo(2L); + } + + @Test + void testCountAll() { + Buildable countStatement = countFrom(person); + + long rows = template.count(countStatement); + assertThat(rows).isEqualTo(6L); + } + + @Test + void testCountLastName() { + Buildable countStatement = countColumn(lastName).from(person); + + long rows = template.count(countStatement); + assertThat(rows).isEqualTo(6L); + } + + @Test + void testCountDistinctLastName() { + Buildable countStatement = countDistinctColumn(lastName).from(person); + + long rows = template.count(countStatement); + assertThat(rows).isEqualTo(2L); + } + + @Test + void testTypeHandledLike() { + Buildable selectStatement = select(id, firstName, lastName, birthDate, employed, occupation, addressId) + .from(person) + .where(lastName, isLike(LastName.of("Fl%"))) + .orderBy(id); + + List rows = template.selectList(selectStatement, personRowMapper); + assertThat(rows).hasSize(3); + assertThat(rows.get(0).getFirstName()).isEqualTo("Fred"); + } + + @Test + void testTypeHandledNotLike() { + Buildable selectStatement = select(id, firstName, lastName, birthDate, employed, occupation, addressId) + .from(person) + .where(lastName, isNotLike(LastName.of("Fl%"))) + .orderBy(id); + + List rows = template.selectList(selectStatement, personRowMapper); + + assertThat(rows).hasSize(3); + assertThat(rows.get(0).getFirstName()).isEqualTo("Barney"); + } + + @Test + void testJoinAllRows() { + Buildable selectStatement = select(id, firstName, lastName, birthDate, employed, occupation, + address.id, address.streetAddress, address.city, address.state) + .from(person) + .join(address, on(person.addressId, equalTo(address.id))) + .orderBy(id); + + List records = template.selectList(selectStatement, personWithAddressRowMapper); + + assertThat(records).hasSize(6); + assertThat(records.get(0).getId()).isEqualTo(1); + assertThat(records.get(0).getEmployed()).isTrue(); + assertThat(records.get(0).getFirstName()).isEqualTo("Fred"); + assertThat(records.get(0).getLastName()).isEqualTo(LastName.of("Flintstone")); + assertThat(records.get(0).getOccupation()).isEqualTo("Brontosaurus Operator"); + assertThat(records.get(0).getBirthDate()).isNotNull(); + assertThat(records.get(0).getAddress().getId()).isEqualTo(1); + assertThat(records.get(0).getAddress().getStreetAddress()).isEqualTo("123 Main Street"); + assertThat(records.get(0).getAddress().getCity()).isEqualTo("Bedrock"); + assertThat(records.get(0).getAddress().getState()).isEqualTo("IN"); + } + + @Test + void testJoinOneRow() { + Buildable selectStatement = select(id, firstName, lastName, birthDate, employed, occupation, + address.id, address.streetAddress, address.city, address.state) + .from(person) + .join(address, on(person.addressId, equalTo(address.id))) + .where(id, isEqualTo(1)); + + List records = template.selectList(selectStatement, personWithAddressRowMapper); + + assertThat(records).hasSize(1); + assertThat(records.get(0).getId()).isEqualTo(1); + assertThat(records.get(0).getEmployed()).isTrue(); + assertThat(records.get(0).getFirstName()).isEqualTo("Fred"); + assertThat(records.get(0).getLastName()).isEqualTo(LastName.of("Flintstone")); + assertThat(records.get(0).getOccupation()).isEqualTo("Brontosaurus Operator"); + assertThat(records.get(0).getBirthDate()).isNotNull(); + assertThat(records.get(0).getAddress().getId()).isEqualTo(1); + assertThat(records.get(0).getAddress().getStreetAddress()).isEqualTo("123 Main Street"); + assertThat(records.get(0).getAddress().getCity()).isEqualTo("Bedrock"); + assertThat(records.get(0).getAddress().getState()).isEqualTo("IN"); + } + + @Test + void testJoinPrimaryKey() { + Buildable selectStatement = select(id, firstName, lastName, birthDate, employed, occupation, + address.id, address.streetAddress, address.city, address.state) + .from(person) + .join(address, on(person.addressId, equalTo(address.id))) + .where(id, isEqualTo(1)); + + Optional record = template.selectOne(selectStatement, personWithAddressRowMapper); + + assertThat(record).hasValueSatisfying(r -> { + assertThat(r.getId()).isEqualTo(1); + assertThat(r.getEmployed()).isTrue(); + assertThat(r.getFirstName()).isEqualTo("Fred"); + assertThat(r.getLastName()).isEqualTo(LastName.of("Flintstone")); + assertThat(r.getOccupation()).isEqualTo("Brontosaurus Operator"); + assertThat(r.getBirthDate()).isNotNull(); + assertThat(r.getAddress().getId()).isEqualTo(1); + assertThat(r.getAddress().getStreetAddress()).isEqualTo("123 Main Street"); + assertThat(r.getAddress().getCity()).isEqualTo("Bedrock"); + assertThat(r.getAddress().getState()).isEqualTo("IN"); + }); + } + + @Test + void testJoinPrimaryKeyInvalidRecord() { + Buildable selectStatement = select(id, firstName, lastName, birthDate, employed, occupation, + address.id, address.streetAddress, address.city, address.state) + .from(person) + .join(address, on(person.addressId, equalTo(address.id))) + .where(id, isEqualTo(55)); + + Optional record = template.selectOne(selectStatement, personWithAddressRowMapper); + assertThat(record).isEmpty(); + } + + @Test + void testJoinCount() { + Buildable countStatement = countFrom(person) + .join(address, on(person.addressId, equalTo(address.id))) + .where(id, isEqualTo(55)); + + long count = template.count(countStatement); + assertThat(count).isZero(); + } + + @Test + void testJoinCountWithSubcriteria() { + Buildable countStatement = countFrom(person) + .join(address, on(person.addressId, equalTo(address.id))) + .where(person.id, isEqualTo(55), or(person.id, isEqualTo(1))); + + long count = template.count(countStatement); + assertThat(count).isEqualTo(1); + } + + private RowMapper personWithAddressRowMapper = + (rs, i) -> { + PersonWithAddress record = new PersonWithAddress(); + record.setId(rs.getInt(1)); + record.setFirstName(rs.getString(2)); + record.setLastName(LastName.of(rs.getString(3))); + record.setBirthDate(rs.getTimestamp(4)); + record.setEmployed("Yes".equals(rs.getString(5)) ? true : false); + record.setOccupation(rs.getString(6)); + + AddressRecord address = new AddressRecord(); + record.setAddress(address); + address.setId(rs.getInt(7)); + address.setStreetAddress(rs.getString(8)); + address.setCity(rs.getString(9)); + address.setState(rs.getString(10)); + + return record; + }; + + + static RowMapper personRowMapper = + (rs, i) -> { + PersonRecord record = new PersonRecord(); + record.setId(rs.getInt(1)); + record.setFirstName(rs.getString(2)); + record.setLastName(LastName.of(rs.getString(3))); + record.setBirthDate(rs.getTimestamp(4)); + record.setEmployed("Yes".equals(rs.getString(5)) ? true : false); + record.setOccupation(rs.getString(6)); + record.setAddressId(rs.getInt(7)); + return record; + }; +} diff --git a/src/test/java/examples/spring/PersonWithAddress.java b/src/test/java/examples/spring/PersonWithAddress.java new file mode 100644 index 000000000..dfe19fe34 --- /dev/null +++ b/src/test/java/examples/spring/PersonWithAddress.java @@ -0,0 +1,84 @@ +/** + * Copyright 2016-2020 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package examples.spring; + +import java.util.Date; + +public class PersonWithAddress { + private Integer id; + private String firstName; + private LastName lastName; + private Date birthDate; + private Boolean employed; + private String occupation; + private AddressRecord address; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public LastName getLastName() { + return lastName; + } + + public void setLastName(LastName lastName) { + this.lastName = lastName; + } + + public Date getBirthDate() { + return birthDate; + } + + public void setBirthDate(Date birthDate) { + this.birthDate = birthDate; + } + + public String getOccupation() { + return occupation; + } + + public void setOccupation(String occupation) { + this.occupation = occupation; + } + + public Boolean getEmployed() { + return employed; + } + + public void setEmployed(Boolean employed) { + this.employed = employed; + } + + public AddressRecord getAddress() { + return address; + } + + public void setAddress(AddressRecord address) { + this.address = address; + } +} diff --git a/src/test/java/examples/spring/ReusableWhereTest.java b/src/test/java/examples/spring/ReusableWhereTest.java new file mode 100644 index 000000000..194b32a7d --- /dev/null +++ b/src/test/java/examples/spring/ReusableWhereTest.java @@ -0,0 +1,93 @@ +/** + * Copyright 2016-2020 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package examples.spring; + +import static examples.spring.PersonDynamicSqlSupport.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mybatis.dynamic.sql.SqlBuilder.*; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mybatis.dynamic.sql.delete.DeleteModel; +import org.mybatis.dynamic.sql.select.SelectModel; +import org.mybatis.dynamic.sql.update.UpdateModel; +import org.mybatis.dynamic.sql.util.Buildable; +import org.mybatis.dynamic.sql.util.spring.NamedParameterJdbcTemplateExtensions; +import org.mybatis.dynamic.sql.where.WhereApplier; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; + +class ReusableWhereTest { + + private NamedParameterJdbcTemplateExtensions template; + + @BeforeEach + void setup() throws Exception { + EmbeddedDatabase db = new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.HSQL) + .generateUniqueName(true) + .addScript("classpath:/examples/simple/CreateSimpleDB.sql") + .build(); + template = new NamedParameterJdbcTemplateExtensions(new NamedParameterJdbcTemplate(db)); + } + + @Test + void testCount() { + Buildable countStatement = countFrom(person) + .applyWhere(commonWhere); + + long rows = template.count(countStatement); + assertThat(rows).isEqualTo(3); + } + + @Test + void testDelete() { + Buildable deleteStatement = deleteFrom(person) + .applyWhere(commonWhere); + + long rows = template.delete(deleteStatement); + + assertThat(rows).isEqualTo(3); + } + + @Test + void testSelect() { + Buildable selectStatement = select(person.allColumns()) + .from(person) + .applyWhere(commonWhere); + + List rows = template.selectList(selectStatement, PersonTemplateTest.personRowMapper); + + assertThat(rows).hasSize(3); + } + + @Test + void testUpdate() { + Buildable updateStatement = update(person) + .set(occupation).equalToStringConstant("worker") + .applyWhere(commonWhere); + + int rows = template.update(updateStatement); + + assertThat(rows).isEqualTo(3); + } + + private WhereApplier commonWhere = d -> d.where(id, isEqualTo(1)).or(occupation, isNull()); +} diff --git a/src/test/kotlin/examples/kotlin/spring/canonical/AddressRecord.kt b/src/test/java/examples/spring/YesNoParameterConverter.java similarity index 68% rename from src/test/kotlin/examples/kotlin/spring/canonical/AddressRecord.kt rename to src/test/java/examples/spring/YesNoParameterConverter.java index bd739226c..4ef3ab651 100644 --- a/src/test/kotlin/examples/kotlin/spring/canonical/AddressRecord.kt +++ b/src/test/java/examples/spring/YesNoParameterConverter.java @@ -13,11 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package examples.kotlin.spring.canonical +package examples.spring; -data class AddressRecord( - var id: Int? = null, - var streetAddress: String? = null, - var city: String? = null, - var state: String? = null -) +import org.mybatis.dynamic.sql.ParameterTypeConverter; + +public class YesNoParameterConverter implements ParameterTypeConverter { + + @Override + public String convert(Boolean source) { + return source == null ? null : source ? "Yes" : "No"; + } +} diff --git a/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonMapperExtensions.kt b/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonMapperExtensions.kt index a0bc2bf5a..3a11687a1 100644 --- a/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonMapperExtensions.kt +++ b/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonMapperExtensions.kt @@ -56,7 +56,7 @@ fun PersonMapper.insert(record: PersonRecord) = map(addressId).toProperty("addressId") } -fun PersonMapper.insert(completer: GeneralInsertCompleter) = +fun PersonMapper.generalInsert(completer: GeneralInsertCompleter) = insertInto(this::generalInsert, Person, completer) fun PersonMapper.insertMultiple(vararg records: PersonRecord) = diff --git a/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonMapperTest.kt b/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonMapperTest.kt index ce6f67c43..36e8b0aca 100644 --- a/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonMapperTest.kt +++ b/src/test/kotlin/examples/kotlin/mybatis3/canonical/PersonMapperTest.kt @@ -204,7 +204,7 @@ class PersonMapperTest { newSession().use { session -> val mapper = session.getMapper(PersonMapper::class.java) - val rows = mapper.insert { + val rows = mapper.generalInsert { set(id).toValue(100) set(firstName).toValue("Joe") set(lastName).toValue(LastName("Jones")) diff --git a/src/test/kotlin/examples/kotlin/spring/canonical/CanonicalSpringKotlinTemplateDirectTest.kt b/src/test/kotlin/examples/kotlin/spring/canonical/CanonicalSpringKotlinTemplateDirectTest.kt index 3169fc769..da190bee1 100644 --- a/src/test/kotlin/examples/kotlin/spring/canonical/CanonicalSpringKotlinTemplateDirectTest.kt +++ b/src/test/kotlin/examples/kotlin/spring/canonical/CanonicalSpringKotlinTemplateDirectTest.kt @@ -16,6 +16,7 @@ package examples.kotlin.spring.canonical import examples.kotlin.spring.canonical.AddressDynamicSqlSupport.Address +import examples.kotlin.spring.canonical.GeneratedAlwaysDynamicSqlSupport.GeneratedAlways import examples.kotlin.spring.canonical.PersonDynamicSqlSupport.Person import examples.kotlin.spring.canonical.PersonDynamicSqlSupport.Person.addressId import examples.kotlin.spring.canonical.PersonDynamicSqlSupport.Person.birthDate @@ -32,6 +33,7 @@ import org.mybatis.dynamic.sql.util.kotlin.spring.* import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType +import org.springframework.jdbc.support.GeneratedKeyHolder import java.util.* class CanonicalSpringKotlinTemplateDirectTest { @@ -39,11 +41,13 @@ class CanonicalSpringKotlinTemplateDirectTest { @BeforeEach fun setup() { - val db = EmbeddedDatabaseBuilder() - .setType(EmbeddedDatabaseType.HSQL) - .generateUniqueName(true) - .addScript("classpath:/examples/kotlin/spring/CreateSimpleDB.sql") - .build() + val db = with(EmbeddedDatabaseBuilder()) { + setType(EmbeddedDatabaseType.HSQL) + generateUniqueName(true) + addScript("classpath:/examples/kotlin/spring/CreateGeneratedAlwaysDB.sql") + addScript("classpath:/examples/kotlin/spring/CreateSimpleDB.sql") + build() + } template = NamedParameterJdbcTemplate(db) } @@ -127,7 +131,7 @@ class CanonicalSpringKotlinTemplateDirectTest { where(id, isLessThan(4)) { or(occupation, isNotNull()) } - and(employed, isEqualTo("Yes")) + and(employed, isEqualTo(true)) } assertThat(rows).isEqualTo(4) @@ -138,7 +142,7 @@ class CanonicalSpringKotlinTemplateDirectTest { val rows = template.deleteFrom(Person) { where(id, isLessThan(4)) or(occupation, isNotNull()) { - and(employed, isEqualTo("Yes")) + and(employed, isEqualTo(true)) } } @@ -150,7 +154,7 @@ class CanonicalSpringKotlinTemplateDirectTest { val rows = template.deleteFrom(Person) { where(id, isLessThan(4)) and(occupation, isNotNull()) { - and(employed, isEqualTo("Yes")) + and(employed, isEqualTo(true)) } } @@ -158,15 +162,32 @@ class CanonicalSpringKotlinTemplateDirectTest { } @Test - fun testInsert() { - val record = PersonRecord(100, "Joe", "Jones", Date(), "Yes", "Developer", 1) + fun testDeprecatedInsert() { + val record = PersonRecord(100, "Joe", LastName("Jones"), Date(), true, "Developer", 1) val rows = template.insert(record, Person) { map(id).toProperty("id") map(firstName).toProperty("firstName") - map(lastName).toProperty("lastName") + map(lastName).toProperty("lastNameAsString") + map(birthDate).toProperty("birthDate") + map(employed).toProperty("employedAsString") + map(occupation).toPropertyWhenPresent("occupation", record::occupation) + map(addressId).toProperty("addressId") + } + + assertThat(rows).isEqualTo(1) + } + + @Test + fun testInsert() { + val record = PersonRecord(100, "Joe", LastName("Jones"), Date(), true, "Developer", 1) + + val rows = template.insert(record).into(Person) { + map(id).toProperty("id") + map(firstName).toProperty("firstName") + map(lastName).toProperty("lastNameAsString") map(birthDate).toProperty("birthDate") - map(employed).toProperty("employed") + map(employed).toProperty("employedAsString") map(occupation).toPropertyWhenPresent("occupation", record::occupation) map(addressId).toProperty("addressId") } @@ -179,9 +200,9 @@ class CanonicalSpringKotlinTemplateDirectTest { val rows = template.insertInto(Person) { set(id).toValue(100) set(firstName).toValue("Joe") - set(lastName).toValue("Jones") + set(lastName).toValue(LastName("Jones")) set(birthDate).toValue(Date()) - set(employed).toValue("Yes") + set(employed).toValue(true) set(occupation).toValue("Developer") set(addressId).toValue(1) } @@ -189,23 +210,106 @@ class CanonicalSpringKotlinTemplateDirectTest { assertThat(rows).isEqualTo(1) } + @Test + fun testMultiRowInsert() { + val record1 = PersonRecord(100, "Joe", LastName("Jones"), Date(), true, "Developer", 1) + val record2 = PersonRecord(101, "Sarah", LastName("Smith"), Date(), true, "Architect", 2) + + val rows = template.insertMultiple(record1, record2).into(Person) { + map(id).toProperty("id") + map(firstName).toProperty("firstName") + map(lastName).toProperty("lastNameAsString") + map(birthDate).toProperty("birthDate") + map(employed).toProperty("employedAsString") + map(occupation).toProperty("occupation") + map(addressId).toProperty("addressId") + } + + assertThat(rows).isEqualTo(2) + } + + @Test + fun testBatchInsert() { + val record1 = PersonRecord(100, "Joe", LastName("Jones"), Date(), true, "Developer", 1) + val record2 = PersonRecord(101, "Sarah", LastName("Smith"), Date(), true, "Architect", 2) + + val rows = template.insertBatch(record1, record2).into(Person) { + map(id).toProperty("id") + map(firstName).toProperty("firstName") + map(lastName).toProperty("lastNameAsString") + map(birthDate).toProperty("birthDate") + map(employed).toProperty("employedAsString") + map(occupation).toProperty("occupation") + map(addressId).toProperty("addressId") + } + + assertThat(rows).hasSize(2) + assertThat(rows[0]).isEqualTo(1) + assertThat(rows[1]).isEqualTo(1) + } + + @Test + fun testGeneralInsertWithGeneratedKey() { + val keyHolder = GeneratedKeyHolder() + + val rows = template.withKeyHolder(keyHolder) { + insertInto(GeneratedAlways) { + set(GeneratedAlways.firstName).toValue("Fred") + set(GeneratedAlways.lastName).toValue("Flintstone") + } + } + + assertThat(rows).isEqualTo(1) + assertThat(keyHolder.keys).containsEntry("ID", 22) + assertThat(keyHolder.keys).containsEntry("FULL_NAME", "Fred Flintstone") + } + + @Test + fun testInsertWithGeneratedKey() { + val record = GeneratedAlwaysRecord(firstName = "Fred", lastName = "Flintstone") + + val keyHolder = GeneratedKeyHolder() + + val rows = template.withKeyHolder(keyHolder) { + insert(record).into(GeneratedAlways) { + map(GeneratedAlways.firstName).toProperty("firstName") + map(GeneratedAlways.lastName).toProperty("lastName") + } + } + + assertThat(rows).isEqualTo(1) + assertThat(keyHolder.keys).containsEntry("ID", 22) + assertThat(keyHolder.keys).containsEntry("FULL_NAME", "Fred Flintstone") + } + + @Test + fun testMultiRowInsertWithGeneratedKey() { + val record1 = GeneratedAlwaysRecord(firstName = "Fred", lastName = "Flintstone") + val record2 = GeneratedAlwaysRecord(firstName = "Barney", lastName = "Rubble") + + val keyHolder = GeneratedKeyHolder() + + val rows = template.withKeyHolder(keyHolder) { + insertMultiple(record1, record2).into(GeneratedAlways) { + map(GeneratedAlways.firstName).toProperty("firstName") + map(GeneratedAlways.lastName).toProperty("lastName") + } + } + + assertThat(rows).isEqualTo(2) + assertThat(keyHolder.keyList[0]).containsEntry("ID", 22) + assertThat(keyHolder.keyList[0]).containsEntry("FULL_NAME", "Fred Flintstone") + assertThat(keyHolder.keyList[1]).containsEntry("ID", 23) + assertThat(keyHolder.keyList[1]).containsEntry("FULL_NAME", "Barney Rubble") + } + @Test fun testSelectAll() { val rows = template.select(id, firstName, lastName, birthDate, employed, occupation, addressId) .from(Person) { allRows() orderBy(id) - }.withRowMapper { rs, _ -> - val record = PersonRecord() - record.id = rs.getInt(1) - record.firstName = rs.getString(2) - record.lastName = rs.getString(3) - record.birthDate = rs.getTimestamp(4) - record.employed = rs.getString(5) - record.occupation = rs.getString(6) - record.addressId = rs.getInt(7) - record - } + }.withRowMapper(personRowMapper) assertThat(rows).hasSize(6) } @@ -221,25 +325,79 @@ class CanonicalSpringKotlinTemplateDirectTest { and(occupation, isNotNull()) orderBy(id) limit(3) - }.withRowMapper { rs, _ -> - val record = PersonRecord() - record.id = rs.getInt(1) - record.firstName = rs.getString(2) - record.lastName = rs.getString(3) - record.birthDate = rs.getTimestamp(4) - record.employed = rs.getString(5) - record.occupation = rs.getString(6) - record.addressId = rs.getInt(7) - record - } + }.withRowMapper(personRowMapper) + + assertThat(rows).hasSize(2) + with(rows[0]) { + assertThat(id).isEqualTo(1) + assertThat(firstName).isEqualTo("Fred") + assertThat(lastName!!.name).isEqualTo("Flintstone") + assertThat(birthDate).isNotNull() + assertThat(employed).isTrue() + assertThat(occupation).isEqualTo("Brontosaurus Operator") + assertThat(addressId).isEqualTo(1) + } + } + + @Test + fun testSelectWithUnion() { + val rows = template.select( + id, firstName, lastName, birthDate, employed, occupation, addressId) + .from(Person) { + where(id, isEqualTo(1)) + union { + select(id, firstName, lastName, birthDate, employed, occupation, addressId) + .from(Person) { + where(id, isEqualTo(2)) + } + } + union { + select(id, firstName, lastName, birthDate, employed, occupation, addressId) + .from(Person) { + where(id, isEqualTo(2)) + } + } + }.withRowMapper(personRowMapper) assertThat(rows).hasSize(2) with(rows[0]) { assertThat(id).isEqualTo(1) assertThat(firstName).isEqualTo("Fred") - assertThat(lastName).isEqualTo("Flintstone") + assertThat(lastName!!.name).isEqualTo("Flintstone") assertThat(birthDate).isNotNull() - assertThat(employed).isEqualTo("Yes") + assertThat(employed).isTrue() + assertThat(occupation).isEqualTo("Brontosaurus Operator") + assertThat(addressId).isEqualTo(1) + } + } + + @Test + fun testSelectWithUnionAll() { + val rows = template.select( + id, firstName, lastName, birthDate, employed, occupation, addressId) + .from(Person) { + where(id, isEqualTo(1)) + union { + select(id, firstName, lastName, birthDate, employed, occupation, addressId) + .from(Person) { + where(id, isEqualTo(2)) + } + } + unionAll { + select(id, firstName, lastName, birthDate, employed, occupation, addressId) + .from(Person) { + where(id, isEqualTo(2)) + } + } + }.withRowMapper(personRowMapper) + + assertThat(rows).hasSize(3) + with(rows[0]) { + assertThat(id).isEqualTo(1) + assertThat(firstName).isEqualTo("Fred") + assertThat(lastName!!.name).isEqualTo("Flintstone") + assertThat(birthDate).isNotNull() + assertThat(employed).isTrue() assertThat(occupation).isEqualTo("Brontosaurus Operator") assertThat(addressId).isEqualTo(1) } @@ -251,24 +409,14 @@ class CanonicalSpringKotlinTemplateDirectTest { id.`as`("A_ID"), firstName, lastName, birthDate, employed, occupation, addressId) .from(Person) { where(id, isEqualTo(1)) - }.withRowMapper { rs, _ -> - val record = PersonRecord() - record.id = rs.getInt(1) - record.firstName = rs.getString(2) - record.lastName = rs.getString(3) - record.birthDate = rs.getTimestamp(4) - record.employed = rs.getString(5) - record.occupation = rs.getString(6) - record.addressId = rs.getInt(7) - record - } + }.withRowMapper(personRowMapper) with(record!!) { assertThat(id).isEqualTo(1) assertThat(firstName).isEqualTo("Fred") - assertThat(lastName).isEqualTo("Flintstone") + assertThat(lastName!!.name).isEqualTo("Flintstone") assertThat(birthDate).isNotNull() - assertThat(employed).isEqualTo("Yes") + assertThat(employed).isTrue() assertThat(occupation).isEqualTo("Brontosaurus Operator") assertThat(addressId).isEqualTo(1) } @@ -322,33 +470,16 @@ class CanonicalSpringKotlinTemplateDirectTest { where(id, isLessThan(4)) orderBy(id) limit(3) - }.withRowMapper { rs, _ -> - val record = PersonWithAddress() - record.id = rs.getInt(1) - record.firstName = rs.getString(2) - record.lastName = rs.getString(3) - record.birthDate = rs.getTimestamp(4) - record.employed = rs.getString(5) - record.occupation = rs.getString(6) - - val address = AddressRecord() - record.address = address - address.id = rs.getInt(7) - address.streetAddress = rs.getString(8) - address.city = rs.getString(9) - address.state = rs.getString(10) - - record - } + }.withRowMapper(personWithAddressRowMapper) assertThat(rows).hasSize(3) with(rows[0]) { assertThat(id).isEqualTo(1) assertThat(firstName).isEqualTo("Fred") - assertThat(lastName).isEqualTo("Flintstone") + assertThat(lastName!!.name).isEqualTo("Flintstone") assertThat(birthDate).isNotNull() - assertThat(employed).isEqualTo("Yes") + assertThat(employed).isTrue() assertThat(occupation).isEqualTo("Brontosaurus Operator") assertThat(address?.id).isEqualTo(1) assertThat(address?.streetAddress).isEqualTo("123 Main Street") @@ -370,25 +501,15 @@ class CanonicalSpringKotlinTemplateDirectTest { } orderBy(id) limit(3) - }.withRowMapper { rs, _ -> - val record = PersonRecord() - record.id = rs.getInt(1) - record.firstName = rs.getString(2) - record.lastName = rs.getString(3) - record.birthDate = rs.getTimestamp(4) - record.employed = rs.getString(5) - record.occupation = rs.getString(6) - record.addressId = rs.getInt(7) - record - } + }.withRowMapper(personRowMapper) assertThat(rows).hasSize(1) with(rows[0]) { assertThat(id).isEqualTo(1) assertThat(firstName).isEqualTo("Fred") - assertThat(lastName).isEqualTo("Flintstone") + assertThat(lastName!!.name).isEqualTo("Flintstone") assertThat(birthDate).isNotNull() - assertThat(employed).isEqualTo("Yes") + assertThat(employed).isTrue() assertThat(occupation).isEqualTo("Brontosaurus Operator") assertThat(addressId).isEqualTo(1) } @@ -407,25 +528,15 @@ class CanonicalSpringKotlinTemplateDirectTest { } orderBy(id) limit(3) - }.withRowMapper { rs, _ -> - val record = PersonRecord() - record.id = rs.getInt(1) - record.firstName = rs.getString(2) - record.lastName = rs.getString(3) - record.birthDate = rs.getTimestamp(4) - record.employed = rs.getString(5) - record.occupation = rs.getString(6) - record.addressId = rs.getInt(7) - record - } + }.withRowMapper(personRowMapper) assertThat(rows).hasSize(3) with(rows[2]) { assertThat(id).isEqualTo(4) assertThat(firstName).isEqualTo("Barney") - assertThat(lastName).isEqualTo("Rubble") + assertThat(lastName!!.name).isEqualTo("Rubble") assertThat(birthDate).isNotNull() - assertThat(employed).isEqualTo("Yes") + assertThat(employed).isTrue() assertThat(occupation).isEqualTo("Brontosaurus Operator") assertThat(addressId).isEqualTo(2) } diff --git a/src/test/kotlin/examples/kotlin/spring/canonical/CanonicalSpringKotlinTest.kt b/src/test/kotlin/examples/kotlin/spring/canonical/CanonicalSpringKotlinTest.kt index 7fbdb562f..e6b5742da 100644 --- a/src/test/kotlin/examples/kotlin/spring/canonical/CanonicalSpringKotlinTest.kt +++ b/src/test/kotlin/examples/kotlin/spring/canonical/CanonicalSpringKotlinTest.kt @@ -16,6 +16,7 @@ package examples.kotlin.spring.canonical import examples.kotlin.spring.canonical.AddressDynamicSqlSupport.Address +import examples.kotlin.spring.canonical.GeneratedAlwaysDynamicSqlSupport.GeneratedAlways import examples.kotlin.spring.canonical.PersonDynamicSqlSupport.Person import examples.kotlin.spring.canonical.PersonDynamicSqlSupport.Person.addressId import examples.kotlin.spring.canonical.PersonDynamicSqlSupport.Person.birthDate @@ -32,19 +33,22 @@ import org.mybatis.dynamic.sql.util.kotlin.spring.* import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType +import org.springframework.jdbc.support.GeneratedKeyHolder import java.util.* -@Suppress("LargeClass", "LongMethod", "MaxLineLength") +@Suppress("LargeClass", "MaxLineLength") class CanonicalSpringKotlinTest { private lateinit var template: NamedParameterJdbcTemplate @BeforeEach fun setup() { - val db = EmbeddedDatabaseBuilder() - .setType(EmbeddedDatabaseType.HSQL) - .generateUniqueName(true) - .addScript("classpath:/examples/kotlin/spring/CreateSimpleDB.sql") - .build() + val db = with(EmbeddedDatabaseBuilder()) { + setType(EmbeddedDatabaseType.HSQL) + generateUniqueName(true) + addScript("classpath:/examples/kotlin/spring/CreateGeneratedAlwaysDB.sql") + addScript("classpath:/examples/kotlin/spring/CreateSimpleDB.sql") + build() + } template = NamedParameterJdbcTemplate(db) } @@ -161,7 +165,7 @@ class CanonicalSpringKotlinTest { where(id, isLessThan(4)) { or(occupation, isNotNull()) } - and(employed, isEqualTo("Yes")) + and(employed, isEqualTo(true)) } val expected = "delete from Person" + @@ -169,6 +173,8 @@ class CanonicalSpringKotlinTest { " and employed = :p2" assertThat(deleteStatement.deleteStatement).isEqualTo(expected) + assertThat(deleteStatement.parameters).containsEntry("p1", 4) + assertThat(deleteStatement.parameters).containsEntry("p2", "Yes") val rows = template.delete(deleteStatement) @@ -180,7 +186,7 @@ class CanonicalSpringKotlinTest { val deleteStatement = deleteFrom(Person) { where(id, isLessThan(4)) or(occupation, isNotNull()) { - and(employed, isEqualTo("Yes")) + and(employed, isEqualTo(true)) } } @@ -201,7 +207,7 @@ class CanonicalSpringKotlinTest { val deleteStatement = deleteFrom(Person) { where(id, isLessThan(4)) and(occupation, isNotNull()) { - and(employed, isEqualTo("Yes")) + and(employed, isEqualTo(true)) } } @@ -219,14 +225,14 @@ class CanonicalSpringKotlinTest { @Test fun testInsert() { - val record = PersonRecord(100, "Joe", "Jones", Date(), "Yes", "Developer", 1) + val record = PersonRecord(100, "Joe", LastName("Jones"), Date(), true, "Developer", 1) val insertStatement = insert(record).into(Person) { map(id).toProperty("id") map(firstName).toProperty("firstName") - map(lastName).toProperty("lastName") + map(lastName).toProperty("lastNameAsString") map(birthDate).toProperty("birthDate") - map(employed).toProperty("employed") + map(employed).toProperty("employedAsString") map(occupation).toProperty("occupation") map(addressId).toProperty("addressId") } @@ -234,8 +240,8 @@ class CanonicalSpringKotlinTest { val expected = "insert into Person (id, first_name, last_name, birth_date, employed, occupation, address_id)" + " values" + " (:id, :firstName," + - " :lastName," + - " :birthDate, :employed," + + " :lastNameAsString," + + " :birthDate, :employedAsString," + " :occupation, :addressId)" assertThat(insertStatement.insertStatement).isEqualTo(expected) @@ -251,9 +257,9 @@ class CanonicalSpringKotlinTest { val insertStatement = insertInto(Person) { set(id).toValue(100) set(firstName).toValue("Joe") - set(lastName).toValue("Jones") + set(lastName).toValue(LastName("Jones")) set(birthDate).toValue(Date()) - set(employed).toValue("Yes") + set(employed).toValue(true) set(occupation).toValue("Developer") set(addressId).toValue(1) } @@ -263,34 +269,125 @@ class CanonicalSpringKotlinTest { assertThat(insertStatement.insertStatement).isEqualTo(expected) - val rows = template.insert(insertStatement) + val rows = template.generalInsert(insertStatement) val record = template.selectOne(id, firstName, lastName, birthDate, employed, occupation, addressId) .from(Person) { where(id, isEqualTo(100)) - }.withRowMapper { rs, _ -> - val record = PersonRecord() - record.id = rs.getInt(1) - record.firstName = rs.getString(2) - record.lastName = rs.getString(3) - record.birthDate = rs.getTimestamp(4) - record.employed = rs.getString(5) - record.occupation = rs.getString(6) - record.addressId = rs.getInt(7) - record - } + }.withRowMapper(personRowMapper) assertThat(rows).isEqualTo(1) with(record!!) { assertThat(id).isEqualTo(100) assertThat(firstName).isEqualTo("Joe") - assertThat(lastName).isEqualTo("Jones") + assertThat(lastName!!.name).isEqualTo("Jones") assertThat(birthDate).isNotNull() - assertThat(employed).isEqualTo("Yes") + assertThat(employed).isTrue() assertThat(occupation).isEqualTo("Developer") assertThat(addressId).isEqualTo(1) } } + @Test + fun testMultiRowInsert() { + val record1 = PersonRecord(100, "Joe", LastName("Jones"), Date(), true, "Developer", 1) + val record2 = PersonRecord(101, "Sarah", LastName("Smith"), Date(), true, "Architect", 2) + + val insertStatement = insertMultiple(record1, record2).into(Person) { + map(id).toProperty("id") + map(firstName).toProperty("firstName") + map(lastName).toProperty("lastNameAsString") + map(birthDate).toProperty("birthDate") + map(employed).toProperty("employedAsString") + map(occupation).toProperty("occupation") + map(addressId).toProperty("addressId") + } + + assertThat(insertStatement.insertStatement).isEqualTo( + "insert into Person (id, first_name, last_name, birth_date, employed, occupation, address_id) " + + "values (:records[0].id, :records[0].firstName, :records[0].lastNameAsString, " + + ":records[0].birthDate, :records[0].employedAsString, :records[0].occupation, :records[0].addressId), " + + "(:records[1].id, :records[1].firstName, :records[1].lastNameAsString, " + + ":records[1].birthDate, :records[1].employedAsString, :records[1].occupation, :records[1].addressId)" + ) + + val rows = template.insertMultiple(insertStatement) + assertThat(rows).isEqualTo(2) + } + + @Test + fun testBatchInsert() { + val record1 = PersonRecord(100, "Joe", LastName("Jones"), Date(), true, "Developer", 1) + val record2 = PersonRecord(101, "Sarah", LastName("Smith"), Date(), true, "Architect", 2) + + val insertStatement = insertBatch(record1, record2).into(Person) { + map(id).toProperty("id") + map(firstName).toProperty("firstName") + map(lastName).toProperty("lastNameAsString") + map(birthDate).toProperty("birthDate") + map(employed).toProperty("employedAsString") + map(occupation).toProperty("occupation") + map(addressId).toProperty("addressId") + } + + val rows = template.insertBatch(insertStatement) + assertThat(rows).hasSize(2) + assertThat(rows[0]).isEqualTo(1) + assertThat(rows[1]).isEqualTo(1) + } + + @Test + fun testGeneralInsertWithGeneratedKey() { + val insertStatement = insertInto(GeneratedAlways) { + set(GeneratedAlways.firstName).toValue("Fred") + set(GeneratedAlways.lastName).toValue("Flintstone") + } + + val keyHolder = GeneratedKeyHolder() + + val rows = template.generalInsert(insertStatement, keyHolder) + assertThat(rows).isEqualTo(1) + assertThat(keyHolder.keys).containsEntry("ID", 22) + assertThat(keyHolder.keys).containsEntry("FULL_NAME", "Fred Flintstone") + } + + @Test + fun testInsertWithGeneratedKey() { + val record = GeneratedAlwaysRecord(firstName = "Fred", lastName = "Flintstone") + + val insertStatement = insert(record).into(GeneratedAlways) { + map(GeneratedAlways.firstName).toProperty("firstName") + map(GeneratedAlways.lastName).toProperty("lastName") + } + + val keyHolder = GeneratedKeyHolder() + + val rows = template.insert(insertStatement, keyHolder) + assertThat(rows).isEqualTo(1) + assertThat(keyHolder.keys).containsEntry("ID", 22) + assertThat(keyHolder.keys).containsEntry("FULL_NAME", "Fred Flintstone") + } + + @Test + fun testMultiRowInsertWithGeneratedKey() { + val record1 = GeneratedAlwaysRecord(firstName = "Fred", lastName = "Flintstone") + val record2 = GeneratedAlwaysRecord(firstName = "Barney", lastName = "Rubble") + + val insertStatement = insertMultiple(record1, record2) + .into(GeneratedAlways) { + map(GeneratedAlways.firstName).toProperty("firstName") + map(GeneratedAlways.lastName).toProperty("lastName") + } + + val keyHolder = GeneratedKeyHolder() + + val rows = template.insertMultiple(insertStatement, keyHolder) + assertThat(rows).isEqualTo(2) + assertThat(keyHolder.keyList[0]).containsEntry("ID", 22) + assertThat(keyHolder.keyList[0]).containsEntry("FULL_NAME", "Fred Flintstone") + assertThat(keyHolder.keyList[1]).containsEntry("ID", 23) + assertThat(keyHolder.keyList[1]).containsEntry("FULL_NAME", "Barney Rubble") + } + @Test fun testRawSelect() { val selectStatement = select( @@ -305,30 +402,34 @@ class CanonicalSpringKotlinTest { limit(3) } - val rows = template.selectList(selectStatement) { rs, _ -> - val record = PersonRecord() - record.id = rs.getInt(1) - record.firstName = rs.getString(2) - record.lastName = rs.getString(3) - record.birthDate = rs.getTimestamp(4) - record.employed = rs.getString(5) - record.occupation = rs.getString(6) - record.addressId = rs.getInt(7) - record - } + val rows = template.selectList(selectStatement, personRowMapper) assertThat(rows).hasSize(2) with(rows[0]) { assertThat(id).isEqualTo(1) assertThat(firstName).isEqualTo("Fred") - assertThat(lastName).isEqualTo("Flintstone") + assertThat(lastName!!.name).isEqualTo("Flintstone") assertThat(birthDate).isNotNull() - assertThat(employed).isEqualTo("Yes") + assertThat(employed).isTrue() assertThat(occupation).isEqualTo("Brontosaurus Operator") assertThat(addressId).isEqualTo(1) } } + @Test + fun testRawSelectWithMissingRecord() { + val selectStatement = select( + id.`as`("A_ID"), firstName, lastName, birthDate, employed, occupation, + addressId + ).from(Person) { + where(id, isEqualTo(300)) + } + + val record = template.selectOne(selectStatement, personRowMapper) + + assertThat(record).isNull() + } + @Test fun testRawSelectByPrimaryKey() { val selectStatement = select( @@ -338,24 +439,14 @@ class CanonicalSpringKotlinTest { where(id, isEqualTo(1)) } - val record = template.selectOne(selectStatement) { rs, _ -> - val record = PersonRecord() - record.id = rs.getInt(1) - record.firstName = rs.getString(2) - record.lastName = rs.getString(3) - record.birthDate = rs.getTimestamp(4) - record.employed = rs.getString(5) - record.occupation = rs.getString(6) - record.addressId = rs.getInt(7) - record - } + val record = template.selectOne(selectStatement, personRowMapper) with(record!!) { assertThat(id).isEqualTo(1) assertThat(firstName).isEqualTo("Fred") - assertThat(lastName).isEqualTo("Flintstone") + assertThat(lastName!!.name).isEqualTo("Flintstone") assertThat(birthDate).isNotNull() - assertThat(employed).isEqualTo("Yes") + assertThat(employed).isTrue() assertThat(occupation).isEqualTo("Brontosaurus Operator") assertThat(addressId).isEqualTo(1) } @@ -400,25 +491,15 @@ class CanonicalSpringKotlinTest { assertThat(selectStatement.selectStatement).isEqualTo(expected) - val records = template.selectList(selectStatement) { rs, _ -> - val record = PersonRecord() - record.id = rs.getInt(1) - record.firstName = rs.getString(2) - record.lastName = rs.getString(3) - record.birthDate = rs.getTimestamp(4) - record.employed = rs.getString(5) - record.occupation = rs.getString(6) - record.addressId = rs.getInt(7) - record - } + val records = template.selectList(selectStatement, personRowMapper) assertThat(records).hasSize(3) with(records[0]) { assertThat(id).isEqualTo(1) assertThat(firstName).isEqualTo("Fred") - assertThat(lastName).isEqualTo("Flintstone") + assertThat(lastName!!.name).isEqualTo("Flintstone") assertThat(birthDate).isNotNull() - assertThat(employed).isEqualTo("Yes") + assertThat(employed).isTrue() assertThat(occupation).isEqualTo("Brontosaurus Operator") assertThat(addressId).isEqualTo(1) } @@ -426,9 +507,9 @@ class CanonicalSpringKotlinTest { with(records[2]) { assertThat(id).isEqualTo(3) assertThat(firstName).isEqualTo("Pebbles") - assertThat(lastName).isEqualTo("Flintstone") + assertThat(lastName!!.name).isEqualTo("Flintstone") assertThat(birthDate).isNotNull() - assertThat(employed).isEqualTo("No") + assertThat(employed).isFalse() assertThat(occupation).isNull() assertThat(addressId).isEqualTo(1) } @@ -473,25 +554,15 @@ class CanonicalSpringKotlinTest { assertThat(selectStatement.selectStatement).isEqualTo(expected) - val records = template.selectList(selectStatement) { rs, _ -> - val record = PersonRecord() - record.id = rs.getInt(1) - record.firstName = rs.getString(2) - record.lastName = rs.getString(3) - record.birthDate = rs.getTimestamp(4) - record.employed = rs.getString(5) - record.occupation = rs.getString(6) - record.addressId = rs.getInt(7) - record - } + val records = template.selectList(selectStatement, personRowMapper) assertThat(records).hasSize(3) with(records[0]) { assertThat(id).isEqualTo(1) assertThat(firstName).isEqualTo("Fred") - assertThat(lastName).isEqualTo("Flintstone") + assertThat(lastName!!.name).isEqualTo("Flintstone") assertThat(birthDate).isNotNull() - assertThat(employed).isEqualTo("Yes") + assertThat(employed).isTrue() assertThat(occupation).isEqualTo("Brontosaurus Operator") assertThat(addressId).isEqualTo(1) } @@ -499,9 +570,9 @@ class CanonicalSpringKotlinTest { with(records[2]) { assertThat(id).isEqualTo(3) assertThat(firstName).isEqualTo("Pebbles") - assertThat(lastName).isEqualTo("Flintstone") + assertThat(lastName!!.name).isEqualTo("Flintstone") assertThat(birthDate).isNotNull() - assertThat(employed).isEqualTo("No") + assertThat(employed).isFalse() assertThat(occupation).isNull() assertThat(addressId).isEqualTo(1) } @@ -546,25 +617,15 @@ class CanonicalSpringKotlinTest { assertThat(selectStatement.selectStatement).isEqualTo(expected) - val records = template.selectList(selectStatement) { rs, _ -> - val record = PersonRecord() - record.id = rs.getInt(1) - record.firstName = rs.getString(2) - record.lastName = rs.getString(3) - record.birthDate = rs.getTimestamp(4) - record.employed = rs.getString(5) - record.occupation = rs.getString(6) - record.addressId = rs.getInt(7) - record - } + val records = template.selectList(selectStatement, personRowMapper) assertThat(records).hasSize(3) with(records[0]) { assertThat(id).isEqualTo(1) assertThat(firstName).isEqualTo("Fred") - assertThat(lastName).isEqualTo("Flintstone") + assertThat(lastName!!.name).isEqualTo("Flintstone") assertThat(birthDate).isNotNull() - assertThat(employed).isEqualTo("Yes") + assertThat(employed).isTrue() assertThat(occupation).isEqualTo("Brontosaurus Operator") assertThat(addressId).isEqualTo(1) } @@ -572,9 +633,9 @@ class CanonicalSpringKotlinTest { with(records[2]) { assertThat(id).isEqualTo(3) assertThat(firstName).isEqualTo("Pebbles") - assertThat(lastName).isEqualTo("Flintstone") + assertThat(lastName!!.name).isEqualTo("Flintstone") assertThat(birthDate).isNotNull() - assertThat(employed).isEqualTo("No") + assertThat(employed).isFalse() assertThat(occupation).isNull() assertThat(addressId).isEqualTo(1) } @@ -620,25 +681,15 @@ class CanonicalSpringKotlinTest { assertThat(selectStatement.selectStatement).isEqualTo(expected) - val records = template.selectList(selectStatement) { rs, _ -> - val record = PersonRecord() - record.id = rs.getInt(1) - record.firstName = rs.getString(2) - record.lastName = rs.getString(3) - record.birthDate = rs.getTimestamp(4) - record.employed = rs.getString(5) - record.occupation = rs.getString(6) - record.addressId = rs.getInt(7) - record - } + val records = template.selectList(selectStatement, personRowMapper) assertThat(records).hasSize(8) with(records[0]) { assertThat(id).isEqualTo(1) assertThat(firstName).isEqualTo("Fred") - assertThat(lastName).isEqualTo("Flintstone") + assertThat(lastName!!.name).isEqualTo("Flintstone") assertThat(birthDate).isNotNull() - assertThat(employed).isEqualTo("Yes") + assertThat(employed).isTrue() assertThat(occupation).isEqualTo("Brontosaurus Operator") assertThat(addressId).isEqualTo(1) } @@ -646,9 +697,9 @@ class CanonicalSpringKotlinTest { with(records[2]) { assertThat(id).isEqualTo(2) assertThat(firstName).isEqualTo("Wilma") - assertThat(lastName).isEqualTo("Flintstone") + assertThat(lastName!!.name).isEqualTo("Flintstone") assertThat(birthDate).isNotNull() - assertThat(employed).isEqualTo("Yes") + assertThat(employed).isTrue() assertThat(occupation).isEqualTo("Accountant") assertThat(addressId).isEqualTo(1) } @@ -676,33 +727,15 @@ class CanonicalSpringKotlinTest { assertThat(selectStatement.selectStatement).isEqualTo(expected) - val rows = template.selectList(selectStatement) { rs, _ -> - val record = PersonWithAddress() - record.id = rs.getInt(1) - record.firstName = rs.getString(2) - record.lastName = rs.getString(3) - record.birthDate = rs.getTimestamp(4) - record.employed = rs.getString(5) - record.occupation = rs.getString(6) + val rows = template.selectList(selectStatement, personWithAddressRowMapper) - val address = AddressRecord() - record.address = address - address.id = rs.getInt(7) - address.streetAddress = rs.getString(8) - address.city = rs.getString(9) - address.state = rs.getString(10) - - record - } - - assertThat(rows).hasSize(3) with(rows[0]) { assertThat(id).isEqualTo(1) assertThat(firstName).isEqualTo("Fred") - assertThat(lastName).isEqualTo("Flintstone") + assertThat(lastName!!.name).isEqualTo("Flintstone") assertThat(birthDate).isNotNull() - assertThat(employed).isEqualTo("Yes") + assertThat(employed).isTrue() assertThat(occupation).isEqualTo("Brontosaurus Operator") assertThat(address?.id).isEqualTo(1) assertThat(address?.streetAddress).isEqualTo("123 Main Street") @@ -736,25 +769,15 @@ class CanonicalSpringKotlinTest { assertThat(selectStatement.selectStatement).isEqualTo(expected) - val rows = template.selectList(selectStatement) { rs, _ -> - val record = PersonRecord() - record.id = rs.getInt(1) - record.firstName = rs.getString(2) - record.lastName = rs.getString(3) - record.birthDate = rs.getTimestamp(4) - record.employed = rs.getString(5) - record.occupation = rs.getString(6) - record.addressId = rs.getInt(7) - record - } + val rows = template.selectList(selectStatement, personRowMapper) assertThat(rows).hasSize(1) with(rows[0]) { assertThat(id).isEqualTo(1) assertThat(firstName).isEqualTo("Fred") - assertThat(lastName).isEqualTo("Flintstone") + assertThat(lastName!!.name).isEqualTo("Flintstone") assertThat(birthDate).isNotNull() - assertThat(employed).isEqualTo("Yes") + assertThat(employed).isTrue() assertThat(occupation).isEqualTo("Brontosaurus Operator") assertThat(addressId).isEqualTo(1) } @@ -785,25 +808,15 @@ class CanonicalSpringKotlinTest { assertThat(selectStatement.selectStatement).isEqualTo(expected) - val rows = template.selectList(selectStatement) { rs, _ -> - val record = PersonRecord() - record.id = rs.getInt(1) - record.firstName = rs.getString(2) - record.lastName = rs.getString(3) - record.birthDate = rs.getTimestamp(4) - record.employed = rs.getString(5) - record.occupation = rs.getString(6) - record.addressId = rs.getInt(7) - record - } + val rows = template.selectList(selectStatement, personRowMapper) assertThat(rows).hasSize(3) with(rows[2]) { assertThat(id).isEqualTo(4) assertThat(firstName).isEqualTo("Barney") - assertThat(lastName).isEqualTo("Rubble") + assertThat(lastName!!.name).isEqualTo("Rubble") assertThat(birthDate).isNotNull() - assertThat(employed).isEqualTo("Yes") + assertThat(employed).isTrue() assertThat(occupation).isEqualTo("Brontosaurus Operator") assertThat(addressId).isEqualTo(2) } @@ -812,15 +825,17 @@ class CanonicalSpringKotlinTest { @Test fun testRawUpdate1() { val updateStatement = update(Person) { - set(firstName).equalTo("Sam") + set(lastName).equalTo(LastName("Smith")) where(firstName, isEqualTo("Fred")) } assertThat(updateStatement.updateStatement).isEqualTo( "update Person" + - " set first_name = :p1" + + " set last_name = :p1" + " where first_name = :p2" ) + assertThat(updateStatement.parameters).containsEntry("p1", "Smith") + assertThat(updateStatement.parameters).containsEntry("p2", "Fred") val rows = template.update(updateStatement) @@ -910,4 +925,76 @@ class CanonicalSpringKotlinTest { assertThat(rows).isEqualTo(2) } + + @Test + fun testUpdateWithTypeConverterAndNullValue() { + val record = PersonRecord(id = 3, firstName = "Sam") + + val updateStatement = update(Person) { + set(firstName).equalTo(record::firstName) + set(lastName).equalTo(record::lastName) + where(id, isEqualTo(record::id)) + } + + assertThat(updateStatement.updateStatement).isEqualTo( + "update Person" + + " set first_name = :p1," + + " last_name = :p2" + + " where id = :p3" + ) + + assertThat(updateStatement.parameters).containsEntry("p1", "Sam") + assertThat(updateStatement.parameters).containsEntry("p2", null) + assertThat(updateStatement.parameters).containsEntry("p3", 3) + + val rows = template.update(updateStatement) + + assertThat(rows).isEqualTo(1) + + val selectStatement = select( + id, firstName, lastName, birthDate, employed, occupation, addressId + ).from(Person) { + where(id, isEqualTo(record::id)) + } + + val returnedRecord = template.selectOne(selectStatement, personRowMapper) + assertThat(returnedRecord).isNotNull() + assertThat(returnedRecord!!.lastName).isNull() + } + + @Test + fun testUpdateWithTypeConverterAndNonNullValue() { + val record = PersonRecord(id = 3, firstName = "Sam", lastName = LastName("Smith")) + + val updateStatement = update(Person) { + set(firstName).equalTo(record::firstName) + set(lastName).equalTo(record::lastName) + where(id, isEqualTo(record::id)) + } + + assertThat(updateStatement.updateStatement).isEqualTo( + "update Person" + + " set first_name = :p1," + + " last_name = :p2" + + " where id = :p3" + ) + + assertThat(updateStatement.parameters).containsEntry("p1", "Sam") + assertThat(updateStatement.parameters).containsEntry("p2", "Smith") + assertThat(updateStatement.parameters).containsEntry("p3", 3) + + val rows = template.update(updateStatement) + + assertThat(rows).isEqualTo(1) + + val selectStatement = select( + id, firstName, lastName, birthDate, employed, occupation, addressId + ).from(Person) { + where(id, isEqualTo(record::id)) + } + + val returnedRecord = template.selectOne(selectStatement, personRowMapper) + assertThat(returnedRecord).isNotNull() + assertThat(returnedRecord!!.lastName?.name).isEqualTo("Smith") + } } diff --git a/src/test/kotlin/examples/kotlin/spring/canonical/DomainAndConverters.kt b/src/test/kotlin/examples/kotlin/spring/canonical/DomainAndConverters.kt new file mode 100644 index 000000000..d43e78adb --- /dev/null +++ b/src/test/kotlin/examples/kotlin/spring/canonical/DomainAndConverters.kt @@ -0,0 +1,64 @@ +/** + * Copyright 2016-2020 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package examples.kotlin.spring.canonical + +import java.util.* + +data class LastName(val name: String) + +val lastNameConverter: (LastName?) -> String? = { it?.name } + +val booleanToStringConverter: (Boolean?) -> String = { it?.let { if (it) "Yes" else "No" } ?: "No" } + +data class PersonRecord( + var id: Int? = null, + var firstName: String? = null, + var lastName: LastName? = null, + var birthDate: Date? = null, + var employed: Boolean? = null, + var occupation: String? = null, + var addressId: Int? = null +) { + val lastNameAsString: String? + get() = lastNameConverter(lastName) + + val employedAsString: String + get() = booleanToStringConverter(employed) +} + +data class PersonWithAddress( + var id: Int? = null, + var firstName: String? = null, + var lastName: LastName? = null, + var birthDate: Date? = null, + var employed: Boolean? = null, + var occupation: String? = null, + var address: AddressRecord? = null +) + +data class AddressRecord( + var id: Int? = null, + var streetAddress: String? = null, + var city: String? = null, + var state: String? = null +) + +data class GeneratedAlwaysRecord( + var id: Int? = null, + var firstName: String? = null, + var lastName: String? = null, + var fullName: String? = null +) diff --git a/src/test/kotlin/examples/kotlin/spring/canonical/PersonWithAddress.kt b/src/test/kotlin/examples/kotlin/spring/canonical/GeneratedAlwaysDynamicSqlSupport.kt similarity index 56% rename from src/test/kotlin/examples/kotlin/spring/canonical/PersonWithAddress.kt rename to src/test/kotlin/examples/kotlin/spring/canonical/GeneratedAlwaysDynamicSqlSupport.kt index ce1c2455e..a75ca3578 100644 --- a/src/test/kotlin/examples/kotlin/spring/canonical/PersonWithAddress.kt +++ b/src/test/kotlin/examples/kotlin/spring/canonical/GeneratedAlwaysDynamicSqlSupport.kt @@ -1,5 +1,5 @@ /** - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2020 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. @@ -15,14 +15,14 @@ */ package examples.kotlin.spring.canonical -import java.util.* +import org.mybatis.dynamic.sql.SqlTable +import java.sql.JDBCType -data class PersonWithAddress( - var id: Int? = null, - var firstName: String? = null, - var lastName: String? = null, - var birthDate: Date? = null, - var employed: String? = null, - var occupation: String? = null, - var address: AddressRecord? = null -) +object GeneratedAlwaysDynamicSqlSupport { + object GeneratedAlways : SqlTable("GeneratedAlways") { + val id = column("id", JDBCType.INTEGER) + val firstName = column("first_name", JDBCType.VARCHAR) + val lastName = column("last_name", JDBCType.VARCHAR) + val fullName = column("full_name", JDBCType.VARCHAR) + } +} diff --git a/src/test/kotlin/examples/kotlin/spring/canonical/PersonDynamicSqlSupport.kt b/src/test/kotlin/examples/kotlin/spring/canonical/PersonDynamicSqlSupport.kt index f32209518..4bee8f838 100644 --- a/src/test/kotlin/examples/kotlin/spring/canonical/PersonDynamicSqlSupport.kt +++ b/src/test/kotlin/examples/kotlin/spring/canonical/PersonDynamicSqlSupport.kt @@ -1,5 +1,5 @@ /** - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2020 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. @@ -23,9 +23,11 @@ object PersonDynamicSqlSupport { object Person : SqlTable("Person") { val id = column("id", JDBCType.INTEGER) val firstName = column("first_name", JDBCType.VARCHAR) - val lastName = column("last_name", JDBCType.VARCHAR) + val lastName = column("last_name", JDBCType.VARCHAR) + .withParameterTypeConverter(lastNameConverter) val birthDate = column("birth_date", JDBCType.DATE) - val employed = column("employed", JDBCType.VARCHAR) + val employed = column("employed", JDBCType.VARCHAR) + .withParameterTypeConverter(booleanToStringConverter) val occupation = column("occupation", JDBCType.VARCHAR) val addressId = column("address_id", JDBCType.INTEGER) } diff --git a/src/test/kotlin/examples/kotlin/spring/canonical/RowMappers.kt b/src/test/kotlin/examples/kotlin/spring/canonical/RowMappers.kt new file mode 100644 index 000000000..6415688c0 --- /dev/null +++ b/src/test/kotlin/examples/kotlin/spring/canonical/RowMappers.kt @@ -0,0 +1,48 @@ +/** + * Copyright 2016-2020 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package examples.kotlin.spring.canonical + +import java.sql.ResultSet + +val personRowMapper: (ResultSet, Int) -> PersonRecord = { rs, _ -> + PersonRecord().apply { + id = rs.getInt(1) + firstName = rs.getString(2) + lastName = rs.getString(3)?.let { LastName(it) } + birthDate = rs.getTimestamp(4) + employed = "Yes" == rs.getString(5) + occupation = rs.getString(6) + addressId = rs.getInt(7) + } +} + +val personWithAddressRowMapper: (ResultSet, Int) -> PersonWithAddress = { rs, _ -> + PersonWithAddress().apply { + id = rs.getInt(1) + firstName = rs.getString(2) + lastName = rs.getString(3)?.let { LastName(it) } + birthDate = rs.getTimestamp(4) + employed = "Yes" == rs.getString(5) + occupation = rs.getString(6) + + address = AddressRecord().apply { + id = rs.getInt(7) + streetAddress = rs.getString(8) + city = rs.getString(9) + state = rs.getString(10) + } + } +} diff --git a/src/test/resources/examples/kotlin/spring/CreateGeneratedAlwaysDB.sql b/src/test/resources/examples/kotlin/spring/CreateGeneratedAlwaysDB.sql new file mode 100644 index 000000000..3de56ae38 --- /dev/null +++ b/src/test/resources/examples/kotlin/spring/CreateGeneratedAlwaysDB.sql @@ -0,0 +1,25 @@ +-- +-- Copyright 2016-2020 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 +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +drop table GeneratedAlways if exists; + +create table GeneratedAlways ( + id int generated by default as identity(start with 22), + first_name varchar(30) not null, + last_name varchar(30) not null, + full_name varchar(61) generated always as (first_name || ' ' || last_name), + primary key(id) +); diff --git a/src/test/resources/examples/kotlin/spring/CreateSimpleDB.sql b/src/test/resources/examples/kotlin/spring/CreateSimpleDB.sql index 88834d2aa..d74c42664 100644 --- a/src/test/resources/examples/kotlin/spring/CreateSimpleDB.sql +++ b/src/test/resources/examples/kotlin/spring/CreateSimpleDB.sql @@ -1,5 +1,5 @@ -- --- Copyright 2016-2019 the original author or authors. +-- Copyright 2016-2020 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. @@ -28,7 +28,7 @@ create table Address ( create table Person ( id int not null, first_name varchar(30) not null, - last_name varchar(30) not null, + last_name varchar(30) null, birth_date date not null, employed varchar(3) not null, occupation varchar(30) null,