Browse Source

Add support for Postgres UUID arrays using JDBC.

We now use Postgres' JDBC drivers TypeInfoCache to register and determine array types including support for UUID.

Closes #1567
pull/1616/head
Mark Paluch 2 years ago committed by Jens Schauder
parent
commit
8e6f33e071
No known key found for this signature in database
GPG Key ID: 9537B67540F0A581
  1. 11
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/aot/JdbcRuntimeHints.java
  2. 5
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultJdbcTypeFactory.java
  3. 12
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcArrayColumns.java
  4. 121
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcPostgresDialect.java
  5. 7
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/package-info.java
  6. 64
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/DefaultJdbcTypeFactoryTest.java

11
spring-data-jdbc/src/main/java/org/springframework/data/jdbc/aot/JdbcRuntimeHints.java

@ -16,11 +16,13 @@
package org.springframework.data.jdbc.aot; package org.springframework.data.jdbc.aot;
import java.util.Arrays; import java.util.Arrays;
import java.util.UUID;
import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.aot.hint.TypeReference; import org.springframework.aot.hint.TypeReference;
import org.springframework.data.jdbc.core.dialect.JdbcPostgresDialect;
import org.springframework.data.jdbc.repository.support.SimpleJdbcRepository; import org.springframework.data.jdbc.repository.support.SimpleJdbcRepository;
import org.springframework.data.relational.auditing.RelationalAuditingCallback; import org.springframework.data.relational.auditing.RelationalAuditingCallback;
import org.springframework.data.relational.core.mapping.event.AfterConvertCallback; import org.springframework.data.relational.core.mapping.event.AfterConvertCallback;
@ -54,5 +56,14 @@ class JdbcRuntimeHints implements RuntimeHintsRegistrar {
TypeReference.of("org.springframework.aop.SpringProxy"), TypeReference.of("org.springframework.aop.SpringProxy"),
TypeReference.of("org.springframework.aop.framework.Advised"), TypeReference.of("org.springframework.aop.framework.Advised"),
TypeReference.of("org.springframework.core.DecoratingProxy")); TypeReference.of("org.springframework.core.DecoratingProxy"));
hints.reflection().registerType(TypeReference.of("org.postgresql.jdbc.TypeInfoCache"),
MemberCategory.PUBLIC_CLASSES);
for (Class<?> simpleType : JdbcPostgresDialect.INSTANCE.simpleTypes()) {
hints.reflection().registerType(TypeReference.of(simpleType), MemberCategory.PUBLIC_CLASSES);
}
hints.reflection().registerType(TypeReference.of(UUID.class.getName()), MemberCategory.PUBLIC_CLASSES);
} }
} }

5
spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultJdbcTypeFactory.java

@ -18,7 +18,6 @@ package org.springframework.data.jdbc.core.convert;
import java.sql.Array; import java.sql.Array;
import java.sql.SQLType; import java.sql.SQLType;
import org.springframework.data.jdbc.support.JdbcUtil;
import org.springframework.jdbc.core.ConnectionCallback; import org.springframework.jdbc.core.ConnectionCallback;
import org.springframework.jdbc.core.JdbcOperations; import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -66,9 +65,9 @@ public class DefaultJdbcTypeFactory implements JdbcTypeFactory {
Assert.notNull(value, "Value must not be null"); Assert.notNull(value, "Value must not be null");
Class<?> componentType = arrayColumns.getArrayType(value.getClass()); Class<?> componentType = arrayColumns.getArrayType(value.getClass());
SQLType jdbcType = arrayColumns.getSqlType(componentType);
SQLType jdbcType = JdbcUtil.targetSqlTypeFor(componentType); Assert.notNull(jdbcType, () -> String.format("Couldn't determine SQLType for %s", componentType));
Assert.notNull(jdbcType, () -> String.format("Couldn't determine JDBCType for %s", componentType));
String typeName = arrayColumns.getArrayTypeName(jdbcType); String typeName = arrayColumns.getArrayTypeName(jdbcType);
return operations.execute((ConnectionCallback<Array>) c -> c.createArrayOf(typeName, value)); return operations.execute((ConnectionCallback<Array>) c -> c.createArrayOf(typeName, value));

12
spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcArrayColumns.java

@ -17,6 +17,7 @@ package org.springframework.data.jdbc.core.convert;
import java.sql.SQLType; import java.sql.SQLType;
import org.springframework.data.jdbc.support.JdbcUtil;
import org.springframework.data.relational.core.dialect.ArrayColumns; import org.springframework.data.relational.core.dialect.ArrayColumns;
/** /**
@ -33,6 +34,17 @@ public interface JdbcArrayColumns extends ArrayColumns {
return ArrayColumns.unwrapComponentType(userType); return ArrayColumns.unwrapComponentType(userType);
} }
/**
* Determine the {@link SQLType} for a given {@link Class array component type}.
*
* @param componentType component type of the array.
* @return the dialect-supported array type.
* @since 3.1.3
*/
default SQLType getSqlType(Class<?> componentType) {
return JdbcUtil.targetSqlTypeFor(getArrayType(componentType));
}
/** /**
* The appropriate SQL type as a String which should be used to represent the given {@link SQLType} in an * The appropriate SQL type as a String which should be used to represent the given {@link SQLType} in an
* {@link java.sql.Array}. Defaults to the name of the argument. * {@link java.sql.Array}. Defaults to the name of the argument.

121
spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcPostgresDialect.java

@ -15,16 +15,29 @@
*/ */
package org.springframework.data.jdbc.core.dialect; package org.springframework.data.jdbc.core.dialect;
import java.sql.Array;
import java.sql.JDBCType; import java.sql.JDBCType;
import java.sql.SQLException;
import java.sql.SQLType; import java.sql.SQLType;
import java.sql.Types;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.UUID;
import org.postgresql.core.Oid;
import org.postgresql.jdbc.TypeInfoCache;
import org.springframework.data.jdbc.core.convert.JdbcArrayColumns; import org.springframework.data.jdbc.core.convert.JdbcArrayColumns;
import org.springframework.data.relational.core.dialect.PostgresDialect; import org.springframework.data.relational.core.dialect.PostgresDialect;
import org.springframework.util.ClassUtils;
/** /**
* JDBC specific Postgres Dialect. * JDBC specific Postgres Dialect.
* *
* @author Jens Schauder * @author Jens Schauder
* @author Mark Paluch
* @since 2.3 * @since 2.3
*/ */
public class JdbcPostgresDialect extends PostgresDialect implements JdbcDialect { public class JdbcPostgresDialect extends PostgresDialect implements JdbcDialect {
@ -40,11 +53,31 @@ public class JdbcPostgresDialect extends PostgresDialect implements JdbcDialect
static class JdbcPostgresArrayColumns implements JdbcArrayColumns { static class JdbcPostgresArrayColumns implements JdbcArrayColumns {
private static final boolean TYPE_INFO_PRESENT = ClassUtils.isPresent("org.postgresql.jdbc.TypeInfoCache",
JdbcPostgresDialect.class.getClassLoader());
private static final TypeInfoWrapper TYPE_INFO_WRAPPER;
static {
TYPE_INFO_WRAPPER = TYPE_INFO_PRESENT ? new TypeInfoCacheWrapper() : new TypeInfoWrapper();
}
@Override @Override
public boolean isSupported() { public boolean isSupported() {
return true; return true;
} }
@Override
public SQLType getSqlType(Class<?> componentType) {
SQLType sqlType = TYPE_INFO_WRAPPER.getArrayTypeMap().get(componentType);
if (sqlType != null) {
return sqlType;
}
return JdbcArrayColumns.super.getSqlType(componentType);
}
@Override @Override
public String getArrayTypeName(SQLType jdbcType) { public String getArrayTypeName(SQLType jdbcType) {
@ -58,4 +91,92 @@ public class JdbcPostgresDialect extends PostgresDialect implements JdbcDialect
return jdbcType.getName(); return jdbcType.getName();
} }
} }
/**
* Wrapper for Postgres types. Defaults to no-op to guard runtimes against absent TypeInfoCache.
*
* @since 3.1.3
*/
static class TypeInfoWrapper {
/**
* @return a type map between a Java array component type and its Postgres type.
*/
Map<Class<?>, SQLType> getArrayTypeMap() {
return Collections.emptyMap();
}
}
/**
* {@link TypeInfoWrapper} backed by {@link TypeInfoCache}.
*
* @since 3.1.3
*/
static class TypeInfoCacheWrapper extends TypeInfoWrapper {
private final Map<Class<?>, SQLType> arrayTypes = new HashMap<>();
public TypeInfoCacheWrapper() {
TypeInfoCache cache = new TypeInfoCache(null, 0);
addWellKnownTypes(cache);
Iterator<String> it = cache.getPGTypeNamesWithSQLTypes();
try {
while (it.hasNext()) {
String pgTypeName = it.next();
int oid = cache.getPGType(pgTypeName);
String javaClassName = cache.getJavaClass(oid);
int arrayOid = cache.getJavaArrayType(pgTypeName);
if (!ClassUtils.isPresent(javaClassName, getClass().getClassLoader())) {
continue;
}
Class<?> javaClass = ClassUtils.forName(javaClassName, getClass().getClassLoader());
// avoid accidental usage of smaller database types that map to the same Java type or generic-typed SQL
// arrays.
if (javaClass == Array.class || javaClass == String.class || javaClass == Integer.class || oid == Oid.OID
|| oid == Oid.MONEY) {
continue;
}
arrayTypes.put(javaClass, new PGSQLType(pgTypeName, arrayOid));
}
} catch (SQLException | ClassNotFoundException e) {
throw new IllegalStateException("Cannot create type info mapping", e);
}
}
private static void addWellKnownTypes(TypeInfoCache cache) {
cache.addCoreType("uuid", Oid.UUID, Types.OTHER, UUID.class.getName(), Oid.UUID_ARRAY);
}
@Override
Map<Class<?>, SQLType> getArrayTypeMap() {
return arrayTypes;
}
record PGSQLType(String name, int oid) implements SQLType {
@Override
public String getName() {
return name;
}
@Override
public String getVendor() {
return "Postgres";
}
@Override
public Integer getVendorTypeNumber() {
return oid;
}
}
}
} }

7
spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/package-info.java

@ -0,0 +1,7 @@
/**
* JDBC-specific Dialect implementations.
*/
@NonNullApi
package org.springframework.data.jdbc.core.dialect;
import org.springframework.lang.NonNullApi;

64
spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/DefaultJdbcTypeFactoryTest.java

@ -0,0 +1,64 @@
/*
* Copyright 2023 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.jdbc.core.convert;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.sql.Array;
import java.sql.SQLException;
import java.util.UUID;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.postgresql.core.BaseConnection;
import org.springframework.data.jdbc.core.dialect.JdbcPostgresDialect;
import org.springframework.jdbc.core.ConnectionCallback;
import org.springframework.jdbc.core.JdbcOperations;
/**
* Unit tests for {@link DefaultJdbcTypeFactory}.
*
* @author Mark Paluch
*/
@ExtendWith(MockitoExtension.class)
class DefaultJdbcTypeFactoryTest {
@Mock JdbcOperations operations;
@Mock BaseConnection connection;
@Test // GH-1567
void shouldProvidePostgresArrayType() throws SQLException {
DefaultJdbcTypeFactory sut = new DefaultJdbcTypeFactory(operations, JdbcPostgresDialect.INSTANCE.getArraySupport());
when(operations.execute(any(ConnectionCallback.class))).thenAnswer(invocation -> {
ConnectionCallback callback = invocation.getArgument(0, ConnectionCallback.class);
return callback.doInConnection(connection);
});
UUID uuids[] = new UUID[] { UUID.randomUUID(), UUID.randomUUID() };
when(connection.createArrayOf("uuid", uuids)).thenReturn(mock(Array.class));
Array array = sut.createArray(uuids);
assertThat(array).isNotNull();
}
}
Loading…
Cancel
Save