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; + } +}