#30 - Address review feedback.

Introduce ArrayColumns type to encapsulate Dialect-specific array support.
Apply array conversion for properties that do not match the native array type.
Add integration tests for Postgres array columns.

Original pull request: #31.
This commit is contained in:
Mark Paluch
2018-12-05 10:07:25 +01:00
committed by Jens Schauder
parent be5383abed
commit 9f68352ac7
9 changed files with 316 additions and 40 deletions

View File

@@ -0,0 +1,53 @@
package org.springframework.data.r2dbc.dialect;
/**
* Interface declaring methods that express how a dialect supports array-typed columns.
*
* @author Mark Paluch
*/
public interface ArrayColumns {
/**
* Returns {@literal true} if the dialect supports array-typed columns.
*
* @return {@literal true} if the dialect supports array-typed columns.
*/
boolean isSupported();
/**
* Translate the {@link Class user type} of an array into the dialect-specific type. This method considers only the
* component type.
*
* @param userType component type of the array.
* @return the dialect-supported array type.
* @throws UnsupportedOperationException if array typed columns are not supported.
* @throws IllegalArgumentException if the {@code userType} is not a supported array type.
*/
Class<?> getArrayType(Class<?> userType);
/**
* Default {@link ArrayColumns} implementation for dialects that do not support array-typed columns.
*/
enum Unsupported implements ArrayColumns {
INSTANCE;
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.dialect.ArrayColumns#isSupported()
*/
@Override
public boolean isSupported() {
return false;
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.dialect.ArrayColumns#getArrayType(java.lang.Class)
*/
@Override
public Class<?> getArrayType(Class<?> userType) {
throw new UnsupportedOperationException("Array types not supported");
}
}
}

View File

@@ -5,6 +5,7 @@ import java.util.Collections;
import java.util.HashSet;
import org.springframework.data.mapping.model.SimpleTypeHolder;
import org.springframework.data.r2dbc.dialect.ArrayColumns.Unsupported;
/**
* Represents a dialect that is implemented by a particular database.
@@ -61,12 +62,11 @@ public interface Dialect {
LimitClause limit();
/**
* Returns {@literal true} whether this dialect supports array-typed column. Collection-typed columns can map their
* content to native array types.
* Returns the array support object that describes how array-typed columns are supported by this dialect.
*
* @return {@literal true} whether this dialect supports array-typed columns.
* @return the array support object that describes how array-typed columns are supported by this dialect.
*/
default boolean supportsArrayColumns() {
return false;
default ArrayColumns getArraySupport() {
return Unsupported.INSTANCE;
}
}

View File

@@ -1,15 +1,20 @@
package org.springframework.data.r2dbc.dialect;
import lombok.RequiredArgsConstructor;
import java.net.InetAddress;
import java.net.URI;
import java.net.URL;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import org.springframework.data.mapping.model.SimpleTypeHolder;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* An SQL dialect for Postgres.
*
@@ -18,7 +23,7 @@ import java.util.UUID;
public class PostgresDialect implements Dialect {
private static final Set<Class<?>> SIMPLE_TYPES = new HashSet<>(
Arrays.asList(List.class, Collection.class, String[].class, UUID.class, URL.class, URI.class, InetAddress.class));
Arrays.asList(UUID.class, URL.class, URI.class, InetAddress.class));
/**
* Singleton instance.
@@ -57,6 +62,8 @@ public class PostgresDialect implements Dialect {
}
};
private final PostgresArrayColumns ARRAY_COLUMNS = new PostgresArrayColumns(getSimpleTypeHolder());
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.dialect.Dialect#getBindMarkersFactory()
@@ -95,10 +102,41 @@ public class PostgresDialect implements Dialect {
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.dialect.Dialect#supportsArrayColumns()
* @see org.springframework.data.r2dbc.dialect.Dialect#getArraySupport()
*/
@Override
public boolean supportsArrayColumns() {
return true;
public ArrayColumns getArraySupport() {
return ARRAY_COLUMNS;
}
@RequiredArgsConstructor
static class PostgresArrayColumns implements ArrayColumns {
private final SimpleTypeHolder simpleTypes;
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.dialect.ArrayColumns#isSupported()
*/
@Override
public boolean isSupported() {
return true;
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.dialect.ArrayColumns#getArrayType(java.lang.Class)
*/
@Override
public Class<?> getArrayType(Class<?> userType) {
Assert.notNull(userType, "Array component type must not be null");
if (!simpleTypes.isSimpleType(userType)) {
throw new IllegalArgumentException("Unsupported array type: " + ClassUtils.getQualifiedName(userType));
}
return ClassUtils.resolvePrimitiveIfNecessary(userType);
}
}
}

View File

@@ -38,6 +38,7 @@ import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Order;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.r2dbc.dialect.ArrayColumns;
import org.springframework.data.r2dbc.dialect.BindMarker;
import org.springframework.data.r2dbc.dialect.BindMarkers;
import org.springframework.data.r2dbc.dialect.Dialect;
@@ -249,7 +250,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
private Object getWriteValue(PersistentPropertyAccessor propertyAccessor, RelationalPersistentProperty property) {
TypeInformation<?> type = property.getTypeInformation();
Object value = relationalConverter.writeValue(propertyAccessor.getProperty(property), type);
Object value = propertyAccessor.getProperty(property);
if (type.isCollectionLike()) {
@@ -260,17 +261,28 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
throw new InvalidDataAccessApiUsageException("Nested entities are not supported");
}
if (!dialect.supportsArrayColumns()) {
ArrayColumns arrayColumns = dialect.getArraySupport();
if (!arrayColumns.isSupported()) {
throw new InvalidDataAccessResourceUsageException(
"Dialect " + dialect.getClass().getName() + " does not support array columns");
}
if (!property.isArray()) {
return getArrayValue(arrayColumns, property, value);
}
Object zeroLengthArray = Array.newInstance(property.getActualType(), 0);
return relationalConverter.getConversionService().convert(value, zeroLengthArray.getClass());
}
return value;
}
private Object getArrayValue(ArrayColumns arrayColumns, RelationalPersistentProperty property, Object value) {
Class<?> targetType = arrayColumns.getArrayType(property.getActualType());
if (!property.isArray() || !property.getActualType().equals(targetType)) {
Object zeroLengthArray = Array.newInstance(targetType, 0);
return relationalConverter.getConversionService().convert(value, zeroLengthArray.getClass());
}
return value;

View File

@@ -96,7 +96,8 @@ public class EntityRowMapper<T> implements BiFunction<Row, RowMetadata, T> {
return readEntityFrom(row, property);
}
return converter.readValue(row.get(prefix + property.getColumnName()), property.getTypeInformation());
Object value = row.get(prefix + property.getColumnName());
return converter.readValue(value, property.getTypeInformation());
} catch (Exception o_O) {
throw new MappingException(String.format("Could not read property %s from result set!", property), o_O);
@@ -156,9 +157,7 @@ public class EntityRowMapper<T> implements BiFunction<Row, RowMetadata, T> {
String column = prefix + property.getColumnName();
try {
Object value = converter.readValue(resultSet.get(column), property.getTypeInformation());
return converter.getConversionService().convert(value, parameter.getType().getType());
return converter.getConversionService().convert(resultSet.get(column), parameter.getType().getType());
} catch (Exception o_O) {
throw new MappingException(String.format("Couldn't read column %s from Row.", column), o_O);
}

View File

@@ -1,8 +1,8 @@
package org.springframework.data.r2dbc.dialect;
import static org.assertj.core.api.Assertions.*;
import static org.assertj.core.api.SoftAssertions.*;
import java.util.Collection;
import java.util.List;
import org.junit.Test;
@@ -28,35 +28,50 @@ public class PostgresDialectUnitTests {
}
@Test // gh-30
public void shouldConsiderCollectionTypesAsSimple() {
public void shouldConsiderSimpleTypes() {
SimpleTypeHolder holder = PostgresDialect.INSTANCE.getSimpleTypeHolder();
assertThat(holder.isSimpleType(List.class)).isTrue();
assertThat(holder.isSimpleType(Collection.class)).isTrue();
assertSoftly(it -> {
it.assertThat(holder.isSimpleType(String.class)).isTrue();
it.assertThat(holder.isSimpleType(int.class)).isTrue();
it.assertThat(holder.isSimpleType(Integer.class)).isTrue();
});
}
@Test // gh-30
public void shouldConsiderStringArrayTypeAsSimple() {
public void shouldSupportArrays() {
SimpleTypeHolder holder = PostgresDialect.INSTANCE.getSimpleTypeHolder();
ArrayColumns arrayColumns = PostgresDialect.INSTANCE.getArraySupport();
assertThat(holder.isSimpleType(String[].class)).isTrue();
assertThat(arrayColumns.isSupported()).isTrue();
}
@Test // gh-30
public void shouldConsiderIntArrayTypeAsSimple() {
@Test // gh-30
public void shouldUseBoxedArrayTypesForPrimitiveTypes() {
SimpleTypeHolder holder = PostgresDialect.INSTANCE.getSimpleTypeHolder();
ArrayColumns arrayColumns = PostgresDialect.INSTANCE.getArraySupport();
assertThat(holder.isSimpleType(int[].class)).isTrue();
}
assertSoftly(it -> {
it.assertThat(arrayColumns.getArrayType(int.class)).isEqualTo(Integer.class);
it.assertThat(arrayColumns.getArrayType(double.class)).isEqualTo(Double.class);
it.assertThat(arrayColumns.getArrayType(String.class)).isEqualTo(String.class);
});
}
@Test // gh-30
public void shouldConsiderIntegerArrayTypeAsSimple() {
@Test // gh-30
public void shouldRejectNonSimpleArrayTypes() {
SimpleTypeHolder holder = PostgresDialect.INSTANCE.getSimpleTypeHolder();
ArrayColumns arrayColumns = PostgresDialect.INSTANCE.getArraySupport();
assertThat(holder.isSimpleType(Integer[].class)).isTrue();
}
assertThatThrownBy(() -> arrayColumns.getArrayType(getClass())).isInstanceOf(IllegalArgumentException.class);
}
@Test // gh-30
public void shouldRejectNestedCollections() {
ArrayColumns arrayColumns = PostgresDialect.INSTANCE.getArraySupport();
assertThatThrownBy(() -> arrayColumns.getArrayType(List.class)).isInstanceOf(IllegalArgumentException.class);
}
}

View File

@@ -33,4 +33,13 @@ public class SqlServerDialectUnitTests {
assertThat(holder.isSimpleType(UUID.class)).isTrue();
}
@Test // gh-30
public void shouldNotSupportArrays() {
ArrayColumns arrayColumns = SqlServerDialect.INSTANCE.getArraySupport();
assertThat(arrayColumns.isSupported()).isFalse();
assertThatThrownBy(() -> arrayColumns.getArrayType(String.class)).isInstanceOf(UnsupportedOperationException.class);
}
}

View File

@@ -0,0 +1,147 @@
/*
* Copyright 2018 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
*
* http://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 io.r2dbc.spi.ConnectionFactory;
import lombok.AllArgsConstructor;
import reactor.test.StepVerifier;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
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.testing.ExternalDatabase;
import org.springframework.data.r2dbc.testing.PostgresTestSupport;
import org.springframework.data.r2dbc.testing.R2dbcIntegrationTestSupport;
import org.springframework.data.relational.core.mapping.Table;
import org.springframework.jdbc.core.JdbcTemplate;
/**
* Integration tests for PostgreSQL-specific features such as array support.
*
* @author Mark Paluch
*/
public class PostgresIntegrationTests extends R2dbcIntegrationTestSupport {
@ClassRule public static final ExternalDatabase database = PostgresTestSupport.database();
DataSource dataSource = PostgresTestSupport.createDataSource(database);
ConnectionFactory connectionFactory = PostgresTestSupport.createConnectionFactory(database);
JdbcTemplate template = createJdbcTemplate(dataSource);
DatabaseClient client = DatabaseClient.create(connectionFactory);
@Before
public void before() {
template.execute("DROP TABLE IF EXISTS with_arrays");
template.execute("CREATE TABLE with_arrays (" //
+ "boxed_array INT[]," //
+ "primitive_array INT[]," //
+ "multidimensional_array INT[]," //
+ "collection_array INT[][])");
}
@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);
insert(withArrays);
selectAndAssert(actual -> {
assertThat(actual.primitiveArray).containsExactly(1, 2, 3);
});
}
@Test // gh-30
public void shouldReadAndWriteBoxedSingleDimensionArrays() {
EntityWithArrays withArrays = new EntityWithArrays(new Integer[] { 1, 2, 3 }, null, null, null);
insert(withArrays);
selectAndAssert(actual -> {
assertThat(actual.boxedArray).containsExactly(1, 2, 3);
});
}
@Test // gh-30
public void shouldReadAndWriteConvertedDimensionArrays() {
EntityWithArrays withArrays = new EntityWithArrays(null, null, null, Arrays.asList(5, 6, 7));
insert(withArrays);
selectAndAssert(actual -> {
assertThat(actual.collectionArray).containsExactly(5, 6, 7);
});
}
@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);
insert(withArrays);
selectAndAssert(actual -> {
assertThat(actual.multidimensionalArray).hasSize(2);
assertThat(actual.multidimensionalArray[0]).containsExactly(1, 2, 3);
assertThat(actual.multidimensionalArray[1]).containsExactly(4, 5, 6);
});
}
private void insert(EntityWithArrays object) {
client.insert() //
.into(EntityWithArrays.class) //
.using(object) //
.then() //
.as(StepVerifier::create) //
.verifyComplete();
}
private void selectAndAssert(Consumer<? super EntityWithArrays> assertion) {
client.select() //
.from(EntityWithArrays.class).fetch() //
.first() //
.as(StepVerifier::create) //
.consumeNextWith(assertion).verifyComplete();
}
@Table("with_arrays")
@AllArgsConstructor
static class EntityWithArrays {
Integer[] boxedArray;
int[] primitiveArray;
int[][] multidimensionalArray;
List<Integer> collectionArray;
}
}

View File

@@ -8,6 +8,7 @@ import io.r2dbc.spi.RowMetadata;
import lombok.RequiredArgsConstructor;
import java.util.List;
import java.util.Set;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -20,6 +21,7 @@ import org.springframework.data.relational.core.mapping.RelationalPersistentEnti
* Unit tests for {@link EntityRowMapper}.
*
* @author Mark Paluch
* @author Jens Schauder
*/
@RunWith(MockitoJUnitRunner.class)
public class EntityRowMapperUnitTests {
@@ -73,7 +75,7 @@ public class EntityRowMapperUnitTests {
public void shouldConvertArrayToSet() {
EntityRowMapper<EntityWithCollection> mapper = getRowMapper(EntityWithCollection.class);
when(rowMock.get("integerSet")).thenReturn((new int[] { 3, 14 }));
when(rowMock.get("integer_set")).thenReturn((new int[] { 3, 14 }));
EntityWithCollection result = mapper.apply(rowMock, metadata);
assertThat(result.integerSet).contains(3, 14);
@@ -83,7 +85,7 @@ public class EntityRowMapperUnitTests {
public void shouldConvertArrayMembers() {
EntityRowMapper<EntityWithCollection> mapper = getRowMapper(EntityWithCollection.class);
when(rowMock.get("primitiveIntegers")).thenReturn((new long[] { 3L, 14L }));
when(rowMock.get("primitive_integers")).thenReturn((new Long[] { 3L, 14L }));
EntityWithCollection result = mapper.apply(rowMock, metadata);
assertThat(result.primitiveIntegers).contains(3, 14);
@@ -93,11 +95,12 @@ public class EntityRowMapperUnitTests {
public void shouldConvertArrayToBoxedArray() {
EntityRowMapper<EntityWithCollection> mapper = getRowMapper(EntityWithCollection.class);
when(rowMock.get("boxedIntegers")).thenReturn((new int[] { 3, 11 }));
when(rowMock.get("boxed_integers")).thenReturn((new int[] { 3, 11 }));
EntityWithCollection result = mapper.apply(rowMock, metadata);
assertThat(result.boxedIntegers).contains(3, 11);
}
@SuppressWarnings("unchecked")
private <T> EntityRowMapper<T> getRowMapper(Class<T> type) {
RelationalPersistentEntity<T> entity = (RelationalPersistentEntity<T>) strategy.getMappingContext()