diff --git a/CHANGELOG.md b/CHANGELOG.md index 12d256051..b462e4ebe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -97,6 +97,7 @@ Kotlin DSL. - Added support for the "exists" and "not exists" operator in where clauses ([#296](https://github.com/mybatis/mybatis-dynamic-sql/pull/296)) - Refactored the built-in conditions ([#331](https://github.com/mybatis/mybatis-dynamic-sql/pull/331)) ([#336](https://github.com/mybatis/mybatis-dynamic-sql/pull/336)) - Added composition functions for WhereApplier ([#335](https://github.com/mybatis/mybatis-dynamic-sql/pull/335)) +- Added a mapping for general insert and update statements that will render null values as "null" in the SQL ([#343](https://github.com/mybatis/mybatis-dynamic-sql/pull/343)) ## Release 1.2.1 - September 29, 2020 diff --git a/src/main/java/org/mybatis/dynamic/sql/insert/GeneralInsertDSL.java b/src/main/java/org/mybatis/dynamic/sql/insert/GeneralInsertDSL.java index 7780b5aa1..9069184f7 100644 --- a/src/main/java/org/mybatis/dynamic/sql/insert/GeneralInsertDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/insert/GeneralInsertDSL.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 the original author or authors. + * Copyright 2016-2021 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. @@ -29,6 +29,7 @@ import org.mybatis.dynamic.sql.util.NullMapping; import org.mybatis.dynamic.sql.util.StringConstantMapping; import org.mybatis.dynamic.sql.util.ValueMapping; +import org.mybatis.dynamic.sql.util.ValueOrNullMapping; import org.mybatis.dynamic.sql.util.ValueWhenPresentMapping; public class GeneralInsertDSL implements Buildable { @@ -88,6 +89,15 @@ public GeneralInsertDSL toValue(Supplier valueSupplier) { return GeneralInsertDSL.this; } + public GeneralInsertDSL toValueOrNull(T value) { + return toValueOrNull(() -> value); + } + + public GeneralInsertDSL toValueOrNull(Supplier valueSupplier) { + insertMappings.add(ValueOrNullMapping.of(column, valueSupplier)); + return GeneralInsertDSL.this; + } + public GeneralInsertDSL toValueWhenPresent(T value) { return toValueWhenPresent(() -> value); } 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 b59ee8235..153012beb 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 @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 the original author or authors. + * Copyright 2016-2021 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. @@ -26,6 +26,7 @@ import org.mybatis.dynamic.sql.util.NullMapping; import org.mybatis.dynamic.sql.util.StringConstantMapping; import org.mybatis.dynamic.sql.util.ValueMapping; +import org.mybatis.dynamic.sql.util.ValueOrNullMapping; import org.mybatis.dynamic.sql.util.ValueWhenPresentMapping; public class GeneralInsertValuePhraseVisitor extends GeneralInsertMappingVisitor> { @@ -39,9 +40,7 @@ public GeneralInsertValuePhraseVisitor(RenderingStrategy renderingStrategy) { @Override public Optional visit(NullMapping mapping) { - return FieldAndValueAndParameters.withFieldName(mapping.columnName()) - .withValuePhrase("null") //$NON-NLS-1$ - .buildOptional(); + return buildNullFragment(mapping); } @Override @@ -63,6 +62,12 @@ public Optional visit(ValueMapping mapping) { return buildValueFragment(mapping, mapping.value()); } + @Override + public Optional visit(ValueOrNullMapping mapping) { + return mapping.value().map(v -> buildValueFragment(mapping, v)) + .orElseGet(() -> buildNullFragment(mapping)); + } + @Override public Optional visit(ValueWhenPresentMapping mapping) { return mapping.value().flatMap(v -> buildValueFragment(mapping, v)); @@ -73,6 +78,12 @@ private Optional buildValueFragment(AbstractColumnMa return buildFragment(mapping, value); } + private Optional buildNullFragment(AbstractColumnMapping mapping) { + return FieldAndValueAndParameters.withFieldName(mapping.columnName()) + .withValuePhrase("null") //$NON-NLS-1$ + .buildOptional(); + } + private Optional buildFragment(AbstractColumnMapping mapping, Object value) { String mapKey = RenderingStrategy.formatParameterMapKey(sequence); diff --git a/src/main/java/org/mybatis/dynamic/sql/update/UpdateDSL.java b/src/main/java/org/mybatis/dynamic/sql/update/UpdateDSL.java index 9daee27c7..357def526 100644 --- a/src/main/java/org/mybatis/dynamic/sql/update/UpdateDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/update/UpdateDSL.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 the original author or authors. + * Copyright 2016-2021 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. @@ -34,6 +34,7 @@ import org.mybatis.dynamic.sql.util.SelectMapping; import org.mybatis.dynamic.sql.util.StringConstantMapping; import org.mybatis.dynamic.sql.util.ValueMapping; +import org.mybatis.dynamic.sql.util.ValueOrNullMapping; import org.mybatis.dynamic.sql.util.ValueWhenPresentMapping; import org.mybatis.dynamic.sql.where.AbstractWhereDSL; import org.mybatis.dynamic.sql.where.AbstractWhereSupport; @@ -126,6 +127,15 @@ public UpdateDSL equalTo(BasicColumn rightColumn) { return UpdateDSL.this; } + public UpdateDSL equalToOrNull(T value) { + return equalToOrNull(() -> value); + } + + public UpdateDSL equalToOrNull(Supplier valueSupplier) { + columnMappings.add(ValueOrNullMapping.of(column, valueSupplier)); + return UpdateDSL.this; + } + public UpdateDSL equalToWhenPresent(T value) { return equalToWhenPresent(() -> value); } 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 f49c7da7f..21fdcca3b 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 @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 the original author or authors. + * Copyright 2016-2021 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. @@ -33,6 +33,7 @@ import org.mybatis.dynamic.sql.util.StringConstantMapping; import org.mybatis.dynamic.sql.util.UpdateMappingVisitor; import org.mybatis.dynamic.sql.util.ValueMapping; +import org.mybatis.dynamic.sql.util.ValueOrNullMapping; import org.mybatis.dynamic.sql.util.ValueWhenPresentMapping; public class SetPhraseVisitor extends UpdateMappingVisitor> { @@ -74,6 +75,16 @@ public Optional visit(ValueMapping mapping) { return buildFragment(mapping, mapping.value()); } + @Override + public Optional visit(ValueOrNullMapping mapping) { + return mapping.value() + .map(v -> buildFragment(mapping, v)) + .orElseGet(() -> FragmentAndParameters + .withFragment(mapping.columnName() + " = null") //$NON-NLS-1$ + .buildOptional() + ); + } + @Override public Optional visit(ValueWhenPresentMapping mapping) { return mapping.value().flatMap(v -> buildFragment(mapping, v)); 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 b84fbe768..b48c629ec 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/ColumnMappingVisitor.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/ColumnMappingVisitor.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 the original author or authors. + * Copyright 2016-2021 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. @@ -38,6 +38,8 @@ public interface ColumnMappingVisitor { R visit(ValueMapping mapping); + R visit(ValueOrNullMapping mapping); + R visit(ValueWhenPresentMapping mapping); R visit(SelectMapping mapping); 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 605efcc3c..426934fc5 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/InsertMappingVisitor.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/InsertMappingVisitor.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 the original author or authors. + * Copyright 2016-2021 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. @@ -21,6 +21,11 @@ public final R visit(ValueMapping mapping) { throw new UnsupportedOperationException(); } + @Override + public final R visit(ValueOrNullMapping mapping) { + throw new UnsupportedOperationException(); + } + @Override public final R visit(ValueWhenPresentMapping mapping) { throw new UnsupportedOperationException(); diff --git a/src/main/java/org/mybatis/dynamic/sql/util/ValueOrNullMapping.java b/src/main/java/org/mybatis/dynamic/sql/util/ValueOrNullMapping.java new file mode 100644 index 000000000..88a4ccc45 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/util/ValueOrNullMapping.java @@ -0,0 +1,48 @@ +/* + * Copyright 2016-2021 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; + +import java.util.Objects; +import java.util.Optional; +import java.util.function.Supplier; + +import org.mybatis.dynamic.sql.SqlColumn; + +public class ValueOrNullMapping extends AbstractColumnMapping { + + private final Supplier valueSupplier; + // keep a reference to the column so we don't lose the type + private final SqlColumn localColumn; + + private ValueOrNullMapping(SqlColumn column, Supplier valueSupplier) { + super(column); + this.valueSupplier = Objects.requireNonNull(valueSupplier); + localColumn = Objects.requireNonNull(column); + } + + public Optional value() { + return Optional.ofNullable(localColumn.convertParameterType(valueSupplier.get())); + } + + @Override + public R accept(ColumnMappingVisitor visitor) { + return visitor.visit(this); + } + + public static ValueOrNullMapping of(SqlColumn column, Supplier valueSupplier) { + return new ValueOrNullMapping<>(column, valueSupplier); + } +} diff --git a/src/test/java/examples/animal/data/AnimalDataTest.java b/src/test/java/examples/animal/data/AnimalDataTest.java index fd7e7cba6..7d3b449e3 100644 --- a/src/test/java/examples/animal/data/AnimalDataTest.java +++ b/src/test/java/examples/animal/data/AnimalDataTest.java @@ -1406,6 +1406,46 @@ void testUpdate() { } } + @Test + void testUpdateValueOrNullWithValue() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class); + AnimalData record = new AnimalData(); + record.setBodyWeight(2.6); + + UpdateStatementProvider updateStatement = update(animalData) + .set(animalName).equalToOrNull("fred") + .where(id, isEqualTo(1)) + .build() + .render(RenderingStrategies.MYBATIS3); + + assertThat(updateStatement.getUpdateStatement()).isEqualTo( + "update AnimalData set animal_name = #{parameters.p1,jdbcType=VARCHAR} where id = #{parameters.p2,jdbcType=INTEGER}"); + int rows = mapper.update(updateStatement); + assertThat(rows).isEqualTo(1); + } + } + + @Test + void testUpdateValueOrNullWithNull() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class); + AnimalData record = new AnimalData(); + record.setBodyWeight(2.6); + + UpdateStatementProvider updateStatement = update(animalData) + .set(animalName).equalToOrNull((String) null) + .where(id, isEqualTo(1)) + .build() + .render(RenderingStrategies.MYBATIS3); + + assertThat(updateStatement.getUpdateStatement()).isEqualTo( + "update AnimalData set animal_name = null where id = #{parameters.p1,jdbcType=INTEGER}"); + int rows = mapper.update(updateStatement); + assertThat(rows).isEqualTo(1); + } + } + @Test void testInsert() { try (SqlSession sqlSession = sqlSessionFactory.openSession()) { @@ -1994,6 +2034,61 @@ void testGeneralInsert() { } } + @Test + void testGeneralInsertValueOrNullWithValue() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class); + + GeneralInsertStatementProvider insertStatement = insertInto(animalData) + .set(id).toValue(101) + .set(animalName).toValueOrNull("Fred") + .set(brainWeight).toConstant("2.2") + .set(bodyWeight).toValue(4.5) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expected = "insert into AnimalData (id, animal_name, brain_weight, body_weight) " + + "values (#{parameters.p1,jdbcType=INTEGER}, #{parameters.p2,jdbcType=VARCHAR}, 2.2, " + + "#{parameters.p3,jdbcType=DOUBLE})"; + + assertThat(insertStatement.getInsertStatement()).isEqualTo(expected); + assertThat(insertStatement.getParameters()).hasSize(3); + assertThat(insertStatement.getParameters()).containsEntry("p1", 101); + assertThat(insertStatement.getParameters()).containsEntry("p2", "Fred"); + assertThat(insertStatement.getParameters()).containsEntry("p3", 4.5); + + int rows = mapper.generalInsert(insertStatement); + assertThat(rows).isEqualTo(1); + } + } + + @Test + void testGeneralInsertValueOrNullWithNull() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class); + + GeneralInsertStatementProvider insertStatement = insertInto(animalData) + .set(id).toValue(101) + .set(animalName).toValueOrNull((String) null) + .set(brainWeight).toConstant("2.2") + .set(bodyWeight).toValue(4.5) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expected = "insert into AnimalData (id, animal_name, brain_weight, body_weight) " + + "values (#{parameters.p1,jdbcType=INTEGER}, null, 2.2, " + + "#{parameters.p2,jdbcType=DOUBLE})"; + + assertThat(insertStatement.getInsertStatement()).isEqualTo(expected); + assertThat(insertStatement.getParameters()).hasSize(2); + assertThat(insertStatement.getParameters()).containsEntry("p1", 101); + assertThat(insertStatement.getParameters()).containsEntry("p2", 4.5); + + int rows = mapper.generalInsert(insertStatement); + assertThat(rows).isEqualTo(1); + } + } + @Test void testUpdateWithSelect() { try (SqlSession sqlSession = sqlSessionFactory.openSession()) { diff --git a/src/test/java/org/mybatis/dynamic/sql/util/ColumnMappingVisitorTest.java b/src/test/java/org/mybatis/dynamic/sql/util/ColumnMappingVisitorTest.java index a3df2688a..1d2a76bb9 100644 --- a/src/test/java/org/mybatis/dynamic/sql/util/ColumnMappingVisitorTest.java +++ b/src/test/java/org/mybatis/dynamic/sql/util/ColumnMappingVisitorTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 the original author or authors. + * Copyright 2016-2021 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. @@ -87,6 +87,15 @@ void testThatInsertVisitorErrorsForValueMapping() { assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> tv.visit(mapping)); } + @Test + void testThatInsertVisitorErrorsForValueOrNullMapping() { + TestTable table = new TestTable(); + InsertVisitor tv = new InsertVisitor(); + ValueOrNullMapping mapping = ValueOrNullMapping.of(table.id, () -> 3); + + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> tv.visit(mapping)); + } + @Test void testThatInsertVisitorErrorsForValueWhenPresentMapping() { TestTable table = new TestTable(); @@ -192,6 +201,11 @@ public String visit(ValueMapping mapping) { return "Value Mapping"; } + @Override + public String visit(ValueOrNullMapping mapping) { + return "Value or Null Mapping"; + } + @Override public String visit(ValueWhenPresentMapping mapping) { return "Value When Present Mapping"; @@ -221,7 +235,7 @@ public String visit(PropertyMapping mapping) { @Override public String visit(PropertyWhenPresentMapping mapping) { - return "Property Whn Present Mapping"; + return "Property When Present Mapping"; } } @@ -246,6 +260,11 @@ public String visit(ValueMapping mapping) { return "Value Mapping"; } + @Override + public String visit(ValueOrNullMapping mapping) { + return "Value or Null Mapping"; + } + @Override public String visit(ValueWhenPresentMapping mapping) { return "Value When Present Mapping";