#161 - Properly convert arrays for Postgres.

We now properly convert array values (single- and multi-dimensional) when inserting rows with arrays.
This commit is contained in:
Mark Paluch
2019-09-05 15:01:10 +02:00
parent b9afd23528
commit 611b73149b
6 changed files with 166 additions and 28 deletions

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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<Object> 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;
}

View File

@@ -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() {
}
}

View File

@@ -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;

View File

@@ -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<Integer> myarray;
}
}