diff --git a/src/main/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverter.java b/src/main/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverter.java index 4bcfb69..18857b4 100644 --- a/src/main/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverter.java +++ b/src/main/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverter.java @@ -19,7 +19,6 @@ import io.r2dbc.spi.ColumnMetadata; import io.r2dbc.spi.Row; import io.r2dbc.spi.RowMetadata; -import java.lang.reflect.Array; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; @@ -39,6 +38,7 @@ import org.springframework.data.mapping.model.ConvertingPropertyAccessor; import org.springframework.data.mapping.model.ParameterValueProvider; import org.springframework.data.r2dbc.mapping.OutboundRow; import org.springframework.data.r2dbc.mapping.SettableValue; +import org.springframework.data.r2dbc.support.ArrayUtils; import org.springframework.data.relational.core.conversion.BasicRelationalConverter; import org.springframework.data.relational.core.conversion.RelationalConverter; import org.springframework.data.relational.core.dialect.ArrayColumns; @@ -352,10 +352,11 @@ public class MappingR2dbcConverter extends BasicRelationalConverter implements R Class targetType = arrayColumns.getArrayType(property.getActualType()); - if (!property.isArray() || !property.getActualType().equals(targetType)) { + if (!property.isArray() || !targetType.isAssignableFrom(value.getClass())) { - Object zeroLengthArray = Array.newInstance(targetType, 0); - return getConversionService().convert(value, zeroLengthArray.getClass()); + int depth = value.getClass().isArray() ? ArrayUtils.getDimensionDepth(value.getClass()) : 1; + Class targetArrayType = ArrayUtils.getArrayClass(targetType, depth); + return getConversionService().convert(value, targetArrayType); } return value; diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/PostgresDialect.java b/src/main/java/org/springframework/data/r2dbc/dialect/PostgresDialect.java index edae7be..c2b96a4 100644 --- a/src/main/java/org/springframework/data/r2dbc/dialect/PostgresDialect.java +++ b/src/main/java/org/springframework/data/r2dbc/dialect/PostgresDialect.java @@ -81,11 +81,16 @@ public class PostgresDialect extends org.springframework.data.relational.core.di @Override public Class getArrayType(Class userType) { - if (!simpleTypeHolder.isSimpleType(userType)) { - throw new IllegalArgumentException("Unsupported array type: " + ClassUtils.getQualifiedName(userType)); + Class typeToUse = userType; + while (typeToUse.getComponentType() != null) { + typeToUse = typeToUse.getComponentType(); } - return this.delegate.getArrayType(userType); + if (!simpleTypeHolder.isSimpleType(typeToUse)) { + throw new IllegalArgumentException("Unsupported array type: " + ClassUtils.getQualifiedName(typeToUse)); + } + + return this.delegate.getArrayType(typeToUse); } } diff --git a/src/main/java/org/springframework/data/r2dbc/query/QueryMapper.java b/src/main/java/org/springframework/data/r2dbc/query/QueryMapper.java index 9107705..4fac533 100644 --- a/src/main/java/org/springframework/data/r2dbc/query/QueryMapper.java +++ b/src/main/java/org/springframework/data/r2dbc/query/QueryMapper.java @@ -181,19 +181,21 @@ public class QueryMapper { return null; } - if (typeInformation.isCollectionLike()) { - this.converter.writeValue(value, typeInformation); - } else if (value instanceof Iterable) { + if (value instanceof Iterable) { List mapped = new ArrayList<>(); for (Object o : (Iterable) value) { - - mapped.add(this.converter.writeValue(o, typeInformation)); + mapped.add(this.converter.writeValue(o, typeInformation.getActualType())); } return mapped; } + if (typeInformation.getType().isAssignableFrom(value.getClass()) + || (typeInformation.getType().isArray() && value.getClass().isArray())) { + return value; + } + return this.converter.writeValue(value, typeInformation); } @@ -419,12 +421,16 @@ public class QueryMapper { return super.getTypeHint(); } - if (this.property.getActualType().isPrimitive()) { - return ClassTypeInformation.from(ClassUtils.resolvePrimitiveIfNecessary(this.property.getActualType())); + if (this.property.getType().isPrimitive()) { + return ClassTypeInformation.from(ClassUtils.resolvePrimitiveIfNecessary(this.property.getType())); } - if (this.property.getActualType().isInterface() - || java.lang.reflect.Modifier.isAbstract(this.property.getActualType().getModifiers())) { + if (this.property.getType().isArray()) { + return this.property.getTypeInformation(); + } + + if (this.property.getType().isInterface() + || (java.lang.reflect.Modifier.isAbstract(this.property.getType().getModifiers()))) { return ClassTypeInformation.OBJECT; } diff --git a/src/main/java/org/springframework/data/r2dbc/support/ArrayUtils.java b/src/main/java/org/springframework/data/r2dbc/support/ArrayUtils.java new file mode 100644 index 0000000..3d4b30c --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/support/ArrayUtils.java @@ -0,0 +1,88 @@ +/* + * 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.support; + +import java.lang.reflect.Array; +import java.util.Arrays; + +import org.springframework.util.Assert; + +/** + * Utilities for array interaction. + * + * @author Mark Paluch + */ +public abstract class ArrayUtils { + + /** + * Determine the number of dimensions for an array object. + * + * @param value the array to inspect, must not be {@literal null}. + * @return number of dimensions. + */ + public static int getDimensionDepth(Object value) { + + Assert.notNull(value, "Value must not be null"); + + return getDimensionDepth(value.getClass()); + } + + /** + * Determine the number of dimensions for an {@code arrayClass}. + * + * @param arrayClass the array type to inspect, must not be {@literal null}. + * @return number of dimensions. + */ + public static int getDimensionDepth(Class arrayClass) { + + Assert.isTrue(arrayClass != null && arrayClass.isArray(), "Array class must be an array"); + + int result = 0; + Class type = arrayClass; + + while (type.isArray()) { + result++; + + type = type.getComponentType(); + } + + return result; + } + + /** + * Create a new empty array with the given number of {@code dimensions}. + * + * @param componentType array component type. + * @param dimensions number of dimensions (depth). + * @return a new empty array with the given number of {@code dimensions}. + */ + public static Class getArrayClass(Class componentType, int dimensions) { + + Assert.notNull(componentType, "Component type must not be null"); + + int[] lengths = new int[dimensions]; + Arrays.fill(lengths, 0); + + return Array.newInstance(componentType, lengths).getClass(); + } + + /** + * Utility constructor. + */ + private ArrayUtils() { + + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/core/PostgresIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/core/PostgresIntegrationTests.java index 139c8db..f2febdc 100644 --- a/src/test/java/org/springframework/data/r2dbc/core/PostgresIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/core/PostgresIntegrationTests.java @@ -29,9 +29,9 @@ import javax.sql.DataSource; import org.junit.Before; import org.junit.ClassRule; -import org.junit.Ignore; import org.junit.Test; -import org.springframework.data.r2dbc.core.DatabaseClient; + +import org.springframework.data.annotation.Id; import org.springframework.data.r2dbc.testing.ExternalDatabase; import org.springframework.data.r2dbc.testing.PostgresTestSupport; import org.springframework.data.r2dbc.testing.R2dbcIntegrationTestSupport; @@ -57,6 +57,7 @@ public class PostgresIntegrationTests extends R2dbcIntegrationTestSupport { template.execute("DROP TABLE IF EXISTS with_arrays"); template.execute("CREATE TABLE with_arrays (" // + + "id serial PRIMARY KEY," // + "boxed_array INT[]," // + "primitive_array INT[]," // + "multidimensional_array INT[]," // @@ -64,10 +65,9 @@ public class PostgresIntegrationTests extends R2dbcIntegrationTestSupport { } @Test // gh-30 - @Ignore("https://github.com/r2dbc/r2dbc-postgresql/issues/40, r2dbc-postgresql returns Object[] instead of Integer[]") public void shouldReadAndWritePrimitiveSingleDimensionArrays() { - EntityWithArrays withArrays = new EntityWithArrays(null, new int[] { 1, 2, 3 }, null, null); + EntityWithArrays withArrays = new EntityWithArrays(null, null, new int[] { 1, 2, 3 }, null, null); insert(withArrays); selectAndAssert(actual -> { @@ -76,10 +76,9 @@ public class PostgresIntegrationTests extends R2dbcIntegrationTestSupport { } @Test // gh-30 - @Ignore("https://github.com/r2dbc/r2dbc-postgresql/issues/67") public void shouldReadAndWriteBoxedSingleDimensionArrays() { - EntityWithArrays withArrays = new EntityWithArrays(new Integer[] { 1, 2, 3 }, null, null, null); + EntityWithArrays withArrays = new EntityWithArrays(null, new Integer[] { 1, 2, 3 }, null, null, null); insert(withArrays); @@ -91,10 +90,9 @@ public class PostgresIntegrationTests extends R2dbcIntegrationTestSupport { } @Test // gh-30 - @Ignore("https://github.com/r2dbc/r2dbc-postgresql/issues/67") public void shouldReadAndWriteConvertedDimensionArrays() { - EntityWithArrays withArrays = new EntityWithArrays(null, null, null, Arrays.asList(5, 6, 7)); + EntityWithArrays withArrays = new EntityWithArrays(null, null, null, null, Arrays.asList(5, 6, 7)); insert(withArrays); @@ -104,10 +102,10 @@ public class PostgresIntegrationTests extends R2dbcIntegrationTestSupport { } @Test // gh-30 - @Ignore("https://github.com/r2dbc/r2dbc-postgresql/issues/42, Multi-dimensional arrays not supported yet") public void shouldReadAndWriteMultiDimensionArrays() { - EntityWithArrays withArrays = new EntityWithArrays(null, null, new int[][] { { 1, 2, 3 }, { 4, 5 } }, null); + EntityWithArrays withArrays = new EntityWithArrays(null, null, null, new int[][] { { 1, 2, 3 }, { 4, 5, 6 } }, + null); insert(withArrays); @@ -142,6 +140,7 @@ public class PostgresIntegrationTests extends R2dbcIntegrationTestSupport { @AllArgsConstructor static class EntityWithArrays { + @Id Integer id; Integer[] boxedArray; int[] primitiveArray; int[][] multidimensionalArray; diff --git a/src/test/java/org/springframework/data/r2dbc/core/PostgresReactiveDataAccessStrategyTests.java b/src/test/java/org/springframework/data/r2dbc/core/PostgresReactiveDataAccessStrategyTests.java index 0cbf243..969e695 100644 --- a/src/test/java/org/springframework/data/r2dbc/core/PostgresReactiveDataAccessStrategyTests.java +++ b/src/test/java/org/springframework/data/r2dbc/core/PostgresReactiveDataAccessStrategyTests.java @@ -15,9 +15,17 @@ */ package org.springframework.data.r2dbc.core; -import org.springframework.data.r2dbc.core.DefaultReactiveDataAccessStrategy; -import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy; +import static org.assertj.core.api.Assertions.*; + +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Test; + import org.springframework.data.r2dbc.dialect.PostgresDialect; +import org.springframework.data.r2dbc.mapping.OutboundRow; /** * {@link PostgresDialect} specific tests for {@link ReactiveDataAccessStrategy}. @@ -32,4 +40,35 @@ public class PostgresReactiveDataAccessStrategyTests extends ReactiveDataAccessS protected ReactiveDataAccessStrategy getStrategy() { return strategy; } + + @Test + public void shouldConvertPrimitiveMultidimensionArrayToWrapper() { + + OutboundRow row = strategy.getOutboundRow(new WithMultidimensionalArray(new int[][] { { 1, 2, 3 }, { 4, 5 } })); + + assertThat(row.get("myarray").hasValue()).isTrue(); + assertThat(row.get("myarray").getValue()).isInstanceOf(Integer[][].class); + } + + @Test + public void shouldConvertCollectionToArray() { + + OutboundRow row = strategy.getOutboundRow(new WithIntegerCollection(Arrays.asList(1, 2, 3))); + + assertThat(row.get("myarray").hasValue()).isTrue(); + assertThat(row.get("myarray").getValue()).isInstanceOf(Integer[].class); + assertThat((Integer[]) row.get("myarray").getValue()).contains(1, 2, 3); + } + + @RequiredArgsConstructor + static class WithMultidimensionalArray { + + final int[][] myarray; + } + + @RequiredArgsConstructor + static class WithIntegerCollection { + + final List myarray; + } }