Spring Boot 4.0.5, Java 25, JDBC.
Hi,
I ran into an issue when trying to map a PostgreSQL composite array column to a collection property in Spring Data JDBC. Given the following schema:
CREATE TYPE my_element AS (
value1 INT,
value2 TEXT
);
CREATE TABLE my_table (
id BIGSERIAL PRIMARY KEY,
elements my_element[]
);
import java.util.List;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.Table;
@Table("my_table")
public record MyTable(@Id Long id, @Column("elements") List<MyElement> elements) {
public record MyElement(int value1, String value2) {}
}
import org.springframework.data.repository.CrudRepository;
public interface MyTableRepository extends CrudRepository<MyTable, Long> {
}
The integration test:
@Test
void postgresCompositeArraysAreMappedCorrectly() {
var myTable = new MyTable(null, List.of(new MyElement(111, "value111"), new MyElement(222, "foo222")));
var saved = myTableRepository.save(myTable); //Throws exception
//Assert later
}
The call to save throws exception:
org.springframework.jdbc.BadSqlGrammarException: PreparedStatementCallback; bad SQL grammar [INSERT INTO "my_element" ("elements", "my_table_key", "value1", "value2") VALUES (?, ?, ?, ?)]
at org.springframework.jdbc.support.SQLStateSQLExceptionTranslator.doTranslate(SQLStateSQLExceptionTranslator.java:134)
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:102)
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:111)
at org.springframework.jdbc.core.JdbcTemplate.translateException(JdbcTemplate.java:1549)
at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:689)
at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:711)
at org.springframework.jdbc.core.JdbcTemplate.batchUpdate(JdbcTemplate.java:1043)
at org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate.batchUpdate(NamedParameterJdbcTemplate.java:379)
at org.springframework.data.jdbc.core.convert.InsertStrategyFactory$DefaultBatchInsertStrategy.execute(InsertStrategyFactory.java:89)
at org.springframework.data.jdbc.core.convert.DefaultDataAccessStrategy.insert(DefaultDataAccessStrategy.java:151)
at org.springframework.data.jdbc.core.JdbcAggregateChangeExecutionContext.executeBatchInsert(JdbcAggregateChangeExecutionContext.java:116)
at org.springframework.data.jdbc.core.AggregateChangeExecutor.execute(AggregateChangeExecutor.java:89)
at org.springframework.data.jdbc.core.AggregateChangeExecutor.lambda$executeSave$0(AggregateChangeExecutor.java:60)
at org.springframework.data.relational.core.conversion.BatchedActions$InsertCombiner.lambda$forEach$1(BatchedActions.java:170)
at java.base/java.util.HashMap.forEach(HashMap.java:1430)
at org.springframework.data.relational.core.conversion.BatchedActions$InsertCombiner.lambda$forEach$0(BatchedActions.java:170)
at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:186)
at java.base/java.util.stream.SortedOps$SizedRefSortingSink.end(SortedOps.java:357)
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:571)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:560)
at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:153)
at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:176)
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:265)
at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:632)
at org.springframework.data.relational.core.conversion.BatchedActions$InsertCombiner.forEach(BatchedActions.java:169)
at org.springframework.data.relational.core.conversion.BatchedActions.forEach(BatchedActions.java:79)
at org.springframework.data.relational.core.conversion.SaveBatchingAggregateChange.forEachAction(SaveBatchingAggregateChange.java:71)
at org.springframework.data.jdbc.core.AggregateChangeExecutor.executeSave(AggregateChangeExecutor.java:60)
at org.springframework.data.jdbc.core.JdbcAggregateTemplate.performSave(JdbcAggregateTemplate.java:587)
at org.springframework.data.jdbc.core.JdbcAggregateTemplate.save(JdbcAggregateTemplate.java:219)
at org.springframework.data.jdbc.repository.support.SimpleJdbcRepository.save(SimpleJdbcRepository.java:74)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359)
at org.springframework.data.repository.core.support.RepositoryMethodInvoker$RepositoryFragmentMethodInvoker.lambda$new$0(RepositoryMethodInvoker.java:278)
at org.springframework.data.repository.core.support.RepositoryMethodInvoker.doInvoke(RepositoryMethodInvoker.java:169)
at org.springframework.data.repository.core.support.RepositoryMethodInvoker.invoke(RepositoryMethodInvoker.java:158)
at org.springframework.data.repository.core.support.RepositoryComposition$RepositoryFragments.invoke(RepositoryComposition.java:545)
at org.springframework.data.repository.core.support.RepositoryComposition.invoke(RepositoryComposition.java:290)
at org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor.invoke(RepositoryFactorySupport.java:690)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:171)
at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:146)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:133)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:371)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:130)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:135)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:222)
at jdk.proxy1/jdk.proxy1.$Proxy158.save(Unknown Source)
at se.myname.myapp.persistence.repository.CourtCaseRepositoryIT.postgresArraysAreMappedCorrectly(CourtCaseRepositoryIT.java:32)
Caused by: java.sql.BatchUpdateException: Batch entry 0 INSERT INTO "my_element" ("elements", "my_table_key", "value1", "value2") VALUES (('1'::int8), ('0'::int4), ('111'::int4), ('value111')) was aborted: ERROR: cannot open relation "my_element"
Detail: This operation is not supported for composite types.
Position: 13 Call getNextException to see other errors in the batch.
at org.postgresql.jdbc.BatchResultHandler.handleError(BatchResultHandler.java:165)
at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:2561)
at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:672)
at org.postgresql.jdbc.PgStatement.internalExecuteBatch(PgStatement.java:892)
at org.postgresql.jdbc.PgStatement.executeBatch(PgStatement.java:916)
at org.postgresql.jdbc.PgPreparedStatement.executeBatch(PgPreparedStatement.java:1778)
at com.zaxxer.hikari.pool.ProxyStatement.executeBatch(ProxyStatement.java:128)
at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeBatch(HikariProxyPreparedStatement.java)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at net.ttddyy.dsproxy.proxy.StatementProxyLogic.performProxyLogic(StatementProxyLogic.java:287)
at net.ttddyy.dsproxy.proxy.ProxyLogicSupport.proceedMethodExecution(ProxyLogicSupport.java:103)
at net.ttddyy.dsproxy.proxy.StatementProxyLogic.invoke(StatementProxyLogic.java:119)
at net.ttddyy.dsproxy.proxy.jdk.PreparedStatementInvocationHandler.invoke(PreparedStatementInvocationHandler.java:37)
at jdk.proxy1/jdk.proxy1.$Proxy133.executeBatch(Unknown Source)
at org.springframework.jdbc.core.JdbcTemplate.lambda$getPreparedStatementCallback$0(JdbcTemplate.java:1609)
at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:670)
... 48 more
Caused by: org.postgresql.util.PSQLException: ERROR: cannot open relation "my_element"
Detail: This operation is not supported for composite types.
Position: 13
at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2875)
at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:2560)
... 63 more
This indicates that the framework interprets List as a child collection and attempts to persist it into a separate table named after the element type. However, in this case my_element is a PostgreSQL composite type, not a table. PostgreSQL then fails with:
ERROR: cannot open relation "my_element"
Detail: This operation is not supported for composite types.
This demonstrates that Spring Data JDBC currently has no way to map a collection property to a single column containing a PostgreSQL composite array (my_element[]). Instead, it always treats List as a relational association. From a PostgreSQL perspective, mapping a collection of structured values into a single column is a valid and common pattern, but it seems unsupported in the current mapping model.
My questions are:
- Is mapping a collection property to a single column (e.g. PostgreSQL composite arrays) considered out of scope for Spring Data JDBC?
- If not, what is the recommended extension point to support this (custom JdbcConverter, overriding MappingJdbcConverter, or something else)?
- Would the team consider adding an official extension mechanism for handling vendor-specific structured column types like this?
- Does the new JdbcClient support this?
I’d be happy to contribute a PR, but would like guidance on the intended direction before proceeding.
Spring Boot 4.0.5, Java 25, JDBC.
Hi,
I ran into an issue when trying to map a PostgreSQL composite array column to a collection property in Spring Data JDBC. Given the following schema:
The integration test:
The call to save throws exception:
This indicates that the framework interprets List as a child collection and attempts to persist it into a separate table named after the element type. However, in this case my_element is a PostgreSQL composite type, not a table. PostgreSQL then fails with:
This demonstrates that Spring Data JDBC currently has no way to map a collection property to a single column containing a PostgreSQL composite array (my_element[]). Instead, it always treats List as a relational association. From a PostgreSQL perspective, mapping a collection of structured values into a single column is a valid and common pattern, but it seems unsupported in the current mapping model.
My questions are:
I’d be happy to contribute a PR, but would like guidance on the intended direction before proceeding.