#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:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user