From 7c9e777d000c5dd86dc915239a11c4cdd9ab2203 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 2 Apr 2019 16:01:41 +0200 Subject: [PATCH] #85 - Consider BigDecimal and BigInteger as simple types. We now consider BigDecimal and BigInteger as simple types and no longer as entities. We also no longer set properties whose value is null. --- .../r2dbc/dialect/R2dbcSimpleTypeHolder.java | 4 +- .../convert/MappingR2dbcConverter.java | 18 +- .../function/convert/R2dbcConverters.java | 70 ++++++ .../convert/R2dbcCustomConversions.java | 12 +- ...stgresReactiveDataAccessStrategyTests.java | 33 +++ ...ReactiveDataAccessStrategyTestSupport.java | 225 ++++++++++++++++++ ...ServerReactiveDataAccessStrategyTests.java | 33 +++ 7 files changed, 388 insertions(+), 7 deletions(-) create mode 100644 src/test/java/org/springframework/data/r2dbc/function/PostgresReactiveDataAccessStrategyTests.java create mode 100644 src/test/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategyTestSupport.java create mode 100644 src/test/java/org/springframework/data/r2dbc/function/SqlServerReactiveDataAccessStrategyTests.java diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/R2dbcSimpleTypeHolder.java b/src/main/java/org/springframework/data/r2dbc/dialect/R2dbcSimpleTypeHolder.java index 469b1ec..3b6aa93 100644 --- a/src/main/java/org/springframework/data/r2dbc/dialect/R2dbcSimpleTypeHolder.java +++ b/src/main/java/org/springframework/data/r2dbc/dialect/R2dbcSimpleTypeHolder.java @@ -17,6 +17,8 @@ package org.springframework.data.r2dbc.dialect; import io.r2dbc.spi.Row; +import java.math.BigDecimal; +import java.math.BigInteger; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; @@ -36,7 +38,7 @@ public class R2dbcSimpleTypeHolder extends SimpleTypeHolder { * Set of R2DBC simple types. */ public static final Set> R2DBC_SIMPLE_TYPES = Collections - .unmodifiableSet(new HashSet<>(Arrays.asList(OutboundRow.class, Row.class))); + .unmodifiableSet(new HashSet<>(Arrays.asList(OutboundRow.class, Row.class, BigInteger.class, BigDecimal.class))); public static final SimpleTypeHolder HOLDER = new R2dbcSimpleTypeHolder(); diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java b/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java index 356d02d..25604da 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java @@ -114,7 +114,11 @@ public class MappingR2dbcConverter extends BasicRelationalConverter implements R continue; } - propertyAccessor.setProperty(property, readFrom(row, property, "")); + Object value = readFrom(row, property, ""); + + if (value != null) { + propertyAccessor.setProperty(property, value); + } } return result; @@ -266,8 +270,7 @@ public class MappingR2dbcConverter extends BasicRelationalConverter implements R private void writeNullInternal(OutboundRow sink, RelationalPersistentProperty property) { - sink.put(property.getColumnName(), - SettableValue.empty(getPotentiallyConvertedSimpleNullType(property.getType()))); + sink.put(property.getColumnName(), SettableValue.empty(getPotentiallyConvertedSimpleNullType(property.getType()))); } private Class getPotentiallyConvertedSimpleNullType(Class type) { @@ -419,7 +422,14 @@ public class MappingR2dbcConverter extends BasicRelationalConverter implements R String column = prefix + property.getColumnName(); try { - return converter.getConversionService().convert(resultSet.get(column), parameter.getType().getType()); + + Object value = resultSet.get(column); + + if (value == null) { + return null; + } + + return converter.getConversionService().convert(value, parameter.getType().getType()); } catch (Exception o_O) { throw new MappingException(String.format("Couldn't read column %s from Row.", column), o_O); } diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcConverters.java b/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcConverters.java index dce488b..8315fbd 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcConverters.java +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcConverters.java @@ -30,6 +30,12 @@ import java.util.UUID; import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConverterFactory; +import org.springframework.data.convert.CustomConversions; +import org.springframework.data.convert.Jsr310Converters; +import org.springframework.data.convert.WritingConverter; +import org.springframework.data.r2dbc.function.convert.R2dbcConverters.RowToNumberConverterFactory.LocalDateConverterOverride; +import org.springframework.data.r2dbc.function.convert.R2dbcConverters.RowToNumberConverterFactory.LocalDateTimeConverterOverride; +import org.springframework.data.r2dbc.function.convert.R2dbcConverters.RowToNumberConverterFactory.LocalTimeConverterOverride; import org.springframework.data.r2dbc.function.convert.R2dbcConverters.RowToNumberConverterFactory.RowToOffsetDateTimeConverter; import org.springframework.data.r2dbc.function.convert.R2dbcConverters.RowToNumberConverterFactory.RowToStringConverter; import org.springframework.data.r2dbc.function.convert.R2dbcConverters.RowToNumberConverterFactory.RowToUuidConverter; @@ -67,6 +73,22 @@ abstract class R2dbcConverters { return converters; } + /** + * @return A list of the registered converters to enforce JSR-310 type usage. + * @see CustomConversions#DEFAULT_CONVERTERS + * @see Jsr310Converters + */ + public static Collection getOverrideConvertersToRegister() { + + List converters = new ArrayList<>(); + + converters.add(LocalDateConverterOverride.INSTANCE); + converters.add(LocalDateTimeConverterOverride.INSTANCE); + converters.add(LocalTimeConverterOverride.INSTANCE); + + return converters; + } + /** * Simple singleton to convert {@link Row}s to their {@link Boolean} representation. * @@ -229,5 +251,53 @@ abstract class R2dbcConverters { return row.get(0, ZonedDateTime.class); } } + + /** + * {@link Converter} override that forces {@link LocalDate} to stay on {@link LocalDate}. + * + * @author Mark Paluch + */ + @WritingConverter + public enum LocalDateConverterOverride implements Converter { + + INSTANCE; + + @Override + public LocalDate convert(LocalDate value) { + return value; + } + } + + /** + * {@link Converter} override that forces {@link LocalDateTime} to stay on {@link LocalDateTime}. + * + * @author Mark Paluch + */ + @WritingConverter + public enum LocalDateTimeConverterOverride implements Converter { + + INSTANCE; + + @Override + public LocalDateTime convert(LocalDateTime value) { + return value; + } + } + + /** + * {@link Converter} override that forces {@link LocalTime} to stay on {@link LocalTime}. + * + * @author Mark Paluch + */ + @WritingConverter + public enum LocalTimeConverterOverride implements Converter { + + INSTANCE; + + @Override + public LocalTime convert(LocalTime value) { + return value; + } + } } } diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcCustomConversions.java b/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcCustomConversions.java index da6be5f..57bacf3 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcCustomConversions.java +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcCustomConversions.java @@ -40,7 +40,7 @@ public class R2dbcCustomConversions extends CustomConversions { * @param converters must not be {@literal null}. */ public R2dbcCustomConversions(Collection converters) { - super(STORE_CONVERSIONS, converters); + super(STORE_CONVERSIONS, appendOverriddes(converters)); } /** @@ -50,6 +50,14 @@ public class R2dbcCustomConversions extends CustomConversions { * @param converters must not be {@literal null}. */ public R2dbcCustomConversions(StoreConversions storeConversions, Collection converters) { - super(storeConversions, converters); + super(storeConversions, appendOverriddes(converters)); + } + + private static Collection appendOverriddes(Collection converters) { + + List objects = new ArrayList<>(converters); + objects.addAll(R2dbcConverters.getOverrideConvertersToRegister()); + + return objects; } } diff --git a/src/test/java/org/springframework/data/r2dbc/function/PostgresReactiveDataAccessStrategyTests.java b/src/test/java/org/springframework/data/r2dbc/function/PostgresReactiveDataAccessStrategyTests.java new file mode 100644 index 0000000..0231787 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/function/PostgresReactiveDataAccessStrategyTests.java @@ -0,0 +1,33 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function; + +import org.springframework.data.r2dbc.dialect.PostgresDialect; + +/** + * {@link PostgresDialect} specific tests for {@link ReactiveDataAccessStrategy}. + * + * @author Mark Paluch + */ +public class PostgresReactiveDataAccessStrategyTests extends ReactiveDataAccessStrategyTestSupport { + + private final ReactiveDataAccessStrategy strategy = new DefaultReactiveDataAccessStrategy(PostgresDialect.INSTANCE); + + @Override + protected ReactiveDataAccessStrategy getStrategy() { + return strategy; + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategyTestSupport.java b/src/test/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategyTestSupport.java new file mode 100644 index 0000000..f0ff090 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategyTestSupport.java @@ -0,0 +1,225 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import io.r2dbc.spi.Row; +import io.r2dbc.spi.RowMetadata; +import lombok.Data; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; +import java.util.UUID; +import java.util.function.BiConsumer; +import java.util.function.Function; + +import org.junit.Test; + +import org.springframework.data.r2dbc.dialect.Dialect; +import org.springframework.data.r2dbc.domain.SettableValue; + +/** + * Abstract base class for {@link Dialect}-aware {@link DefaultReactiveDataAccessStrategy} tests. + * + * @author Mark Paluch + */ +public abstract class ReactiveDataAccessStrategyTestSupport { + + protected abstract ReactiveDataAccessStrategy getStrategy(); + + @Test // gh-85 + public void shouldReadAndWriteString() { + testType(PrimitiveTypes::setString, PrimitiveTypes::getString, "foo", "string"); + } + + @Test // gh-85 + public void shouldReadAndWriteCharacter() { + testType(PrimitiveTypes::setCharacter, PrimitiveTypes::getCharacter, 'f', "character"); + } + + @Test // gh-85 + public void shouldReadAndWriteBoolean() { + testType(PrimitiveTypes::setBooleanValue, PrimitiveTypes::isBooleanValue, true, "boolean_value"); + } + + @Test // gh-85 + public void shouldReadAndWriteBoxedBoolean() { + testType(PrimitiveTypes::setBoxedBooleanValue, PrimitiveTypes::getBoxedBooleanValue, true, "boxed_boolean_value"); + } + + @Test // gh-85 + public void shouldReadAndWriteByte() { + testType(PrimitiveTypes::setByteValue, PrimitiveTypes::getByteValue, (byte) 123, "byte_value"); + } + + @Test // gh-85 + public void shouldReadAndWriteBoxedByte() { + testType(PrimitiveTypes::setBoxedByteValue, PrimitiveTypes::getBoxedByteValue, (byte) 123, "boxed_byte_value"); + } + + @Test // gh-85 + public void shouldReadAndWriteShort() { + testType(PrimitiveTypes::setShortValue, PrimitiveTypes::getShortValue, (short) 123, "short_value"); + } + + @Test // gh-85 + public void shouldReadAndWriteBoxedShort() { + testType(PrimitiveTypes::setBoxedShortValue, PrimitiveTypes::getBoxedShortValue, (short) 123, "boxed_short_value"); + } + + @Test // gh-85 + public void shouldReadAndWriteInteger() { + testType(PrimitiveTypes::setIntValue, PrimitiveTypes::getIntValue, 123, "int_value"); + } + + @Test // gh-85 + public void shouldReadAndWriteBoxedInteger() { + testType(PrimitiveTypes::setBoxedIntegerValue, PrimitiveTypes::getBoxedIntegerValue, 123, "boxed_integer_value"); + } + + @Test // gh-85 + public void shouldReadAndWriteLong() { + testType(PrimitiveTypes::setLongValue, PrimitiveTypes::getLongValue, 123L, "long_value"); + } + + @Test // gh-85 + public void shouldReadAndWriteBoxedLong() { + testType(PrimitiveTypes::setBoxedLongValue, PrimitiveTypes::getBoxedLongValue, 123L, "boxed_long_value"); + } + + @Test // gh-85 + public void shouldReadAndWriteFloat() { + testType(PrimitiveTypes::setFloatValue, PrimitiveTypes::getFloatValue, 0.1f, "float_value"); + } + + @Test // gh-85 + public void shouldReadAndWriteBoxedFloat() { + testType(PrimitiveTypes::setBoxedFloatValue, PrimitiveTypes::getBoxedFloatValue, 0.1f, "boxed_float_value"); + } + + @Test // gh-85 + public void shouldReadAndWriteDouble() { + testType(PrimitiveTypes::setDoubleValue, PrimitiveTypes::getDoubleValue, 0.1, "double_value"); + } + + @Test // gh-85 + public void shouldReadAndWriteBoxedDouble() { + testType(PrimitiveTypes::setBoxedDoubleValue, PrimitiveTypes::getBoxedDoubleValue, 0.1, "boxed_double_value"); + } + + @Test // gh-85 + public void shouldReadAndWriteBigInteger() { + testType(PrimitiveTypes::setBigInteger, PrimitiveTypes::getBigInteger, BigInteger.TEN, "big_integer"); + } + + @Test // gh-85 + public void shouldReadAndWriteBigDecimal() { + testType(PrimitiveTypes::setBigDecimal, PrimitiveTypes::getBigDecimal, new BigDecimal("100.123"), "big_decimal"); + } + + @Test // gh-85 + public void shouldReadAndWriteLocalDate() { + testType(PrimitiveTypes::setLocalDate, PrimitiveTypes::getLocalDate, LocalDate.now(), "local_date"); + } + + @Test // gh-85 + public void shouldReadAndWriteLocalTime() { + testType(PrimitiveTypes::setLocalTime, PrimitiveTypes::getLocalTime, LocalTime.now(), "local_time"); + } + + @Test // gh-85 + public void shouldReadAndWriteLocalDateTime() { + testType(PrimitiveTypes::setLocalDateTime, PrimitiveTypes::getLocalDateTime, LocalDateTime.now(), + "local_date_time"); + } + + @Test // gh-85 + public void shouldReadAndWriteZonedDateTime() { + testType(PrimitiveTypes::setZonedDateTime, PrimitiveTypes::getZonedDateTime, ZonedDateTime.now(), + "zoned_date_time"); + } + + @Test // gh-85 + public void shouldReadAndWriteOffsetDateTime() { + testType(PrimitiveTypes::setOffsetDateTime, PrimitiveTypes::getOffsetDateTime, OffsetDateTime.now(), + "offset_date_time"); + } + + @Test // gh-85 + public void shouldReadAndWriteUuid() { + testType(PrimitiveTypes::setUuid, PrimitiveTypes::getUuid, UUID.randomUUID(), "uuid"); + } + + private void testType(BiConsumer setter, Function getter, T testValue, + String fieldname) { + + ReactiveDataAccessStrategy strategy = getStrategy(); + Row rowMock = mock(Row.class); + RowMetadata metadataMock = mock(RowMetadata.class); + + PrimitiveTypes toSave = new PrimitiveTypes(); + setter.accept(toSave, testValue); + + assertThat(strategy.getOutboundRow(toSave)).containsEntry(fieldname, SettableValue.from(testValue)); + + when(rowMock.get(fieldname)).thenReturn(testValue); + + PrimitiveTypes loaded = strategy.getRowMapper(PrimitiveTypes.class).apply(rowMock, metadataMock); + + assertThat(getter.apply(loaded)).isEqualTo(testValue); + } + + @Data + static class PrimitiveTypes { + + String string; + char character; + + boolean booleanValue; + byte byteValue; + short shortValue; + int intValue; + long longValue; + double doubleValue; + float floatValue; + + Boolean boxedBooleanValue; + Byte boxedByteValue; + Short boxedShortValue; + Integer boxedIntegerValue; + Long boxedLongValue; + Double boxedDoubleValue; + Float boxedFloatValue; + + BigInteger bigInteger; + BigDecimal bigDecimal; + + LocalDate localDate; + LocalTime localTime; + LocalDateTime localDateTime; + OffsetDateTime offsetDateTime; + ZonedDateTime zonedDateTime; + + UUID uuid; + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/function/SqlServerReactiveDataAccessStrategyTests.java b/src/test/java/org/springframework/data/r2dbc/function/SqlServerReactiveDataAccessStrategyTests.java new file mode 100644 index 0000000..a1766d0 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/function/SqlServerReactiveDataAccessStrategyTests.java @@ -0,0 +1,33 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function; + +import org.springframework.data.r2dbc.dialect.SqlServerDialect; + +/** + * {@link SqlServerDialect} specific tests for {@link ReactiveDataAccessStrategy}. + * + * @author Mark Paluch + */ +public class SqlServerReactiveDataAccessStrategyTests extends ReactiveDataAccessStrategyTestSupport { + + private final ReactiveDataAccessStrategy strategy = new DefaultReactiveDataAccessStrategy(SqlServerDialect.INSTANCE); + + @Override + protected ReactiveDataAccessStrategy getStrategy() { + return strategy; + } +}