diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/AbstractPostgreSQLStructJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/AbstractPostgreSQLStructJdbcType.java index 76b67da320fa..ee6dfa3d06cb 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/AbstractPostgreSQLStructJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/AbstractPostgreSQLStructJdbcType.java @@ -1156,6 +1156,9 @@ private static String unescape(CharSequence string, int start, int end) { @Override public Object createJdbcValue(Object domainValue, WrapperOptions options) throws SQLException { assert embeddableMappingType != null; + if (domainValue == null) { + return null; + } final StringBuilder sb = new StringBuilder(); serializeStructTo( new PostgreSQLAppender( sb ), domainValue, options ); return sb.toString(); @@ -1165,7 +1168,11 @@ public Object createJdbcValue(Object domainValue, WrapperOptions options) throws public Object[] extractJdbcValues(Object rawJdbcValue, WrapperOptions options) throws SQLException { assert embeddableMappingType != null; final Object[] array = new Object[embeddableMappingType.getJdbcValueCount()]; - deserializeStruct( getRawStructFromJdbcValue( rawJdbcValue ), 0, 0, array, true, options ); + final String struct = getRawStructFromJdbcValue( rawJdbcValue ); + if ( struct == null ) { + return null; + } + deserializeStruct( struct, 0, 0, array, true, options ); if ( inverseOrderMapping != null ) { StructHelper.orderJdbcValues( embeddableMappingType, inverseOrderMapping, array.clone(), array ); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/StructHelper.java b/hibernate-core/src/main/java/org/hibernate/dialect/StructHelper.java index 8458d4adbfc3..9af79531a4f8 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/StructHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/StructHelper.java @@ -105,6 +105,9 @@ public static Object[] getJdbcValues( WrapperOptions options) throws SQLException { final int jdbcValueCount = embeddableMappingType.getJdbcValueCount(); final int valueCount = jdbcValueCount + ( embeddableMappingType.isPolymorphic() ? 1 : 0 ); + if (domainValue == null) { + return null; + } final Object[] values = embeddableMappingType.getValues( domainValue ); final Object[] jdbcValues; if ( valueCount != values.length || orderMapping != null ) { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/StructJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/StructJdbcType.java index 2ae5ed01f137..6f5fac124402 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/StructJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/StructJdbcType.java @@ -141,11 +141,13 @@ public Object createJdbcValue(Object domainValue, WrapperOptions options) throws domainValue, options ); - return options.getSession() + return jdbcValues == null + ? null + : options.getSession() .getJdbcCoordinator() .getLogicalConnection() .getPhysicalConnection() - .createStruct( typeName, jdbcValues ); + .createStruct(typeName, jdbcValues); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/EmbeddableAggregateJavaType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/EmbeddableAggregateJavaType.java index 812bfa9d45c5..1fd0388fc5a7 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/EmbeddableAggregateJavaType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/EmbeddableAggregateJavaType.java @@ -85,6 +85,9 @@ public X unwrap(T value, Class type, WrapperOptions options) { @Override public T wrap(X value, WrapperOptions options) { + if ( value == null ) { + return null; + } if ( getJavaTypeClass().isInstance( value ) ) { //noinspection unchecked return (T) value; diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/ArrayJdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/ArrayJdbcType.java index d4c005df983a..84b7be319856 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/ArrayJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/ArrayJdbcType.java @@ -191,13 +191,19 @@ protected X getArray(BasicExtractor extractor, java.sql.Array array, Wrap final Object rawArray = array.getArray(); final Object[] domainObjects = new Object[Array.getLength( rawArray )]; for ( int i = 0; i < domainObjects.length; i++ ) { - final Object[] aggregateRawValues = aggregateJdbcType.extractJdbcValues( Array.get( rawArray, i ), options ); - final StructAttributeValues attributeValues = StructHelper.getAttributeValues( - embeddableMappingType, - aggregateRawValues, - options - ); - domainObjects[i] = instantiate( embeddableMappingType, attributeValues, options.getSessionFactory() ); + final Object rawJdbcValue = Array.get(rawArray, i); + if (rawJdbcValue == null) { + domainObjects[i] = null; + } + else { + final Object[] aggregateRawValues = aggregateJdbcType.extractJdbcValues(rawJdbcValue, options); + final StructAttributeValues attributeValues = StructHelper.getAttributeValues( + embeddableMappingType, + aggregateRawValues, + options + ); + domainObjects[i] = instantiate( embeddableMappingType, attributeValues, options.getSessionFactory() ); + } } return extractor.getJavaType().wrap( domainObjects, options ); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/array/StructArrayWithNullElementTestDemoTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/array/StructArrayWithNullElementTestDemoTest.java new file mode 100644 index 000000000000..e44cf3906826 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/array/StructArrayWithNullElementTestDemoTest.java @@ -0,0 +1,87 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.array; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.annotations.Struct; +import org.hibernate.testing.orm.junit.*; +import org.hibernate.testing.orm.junit.DialectFeatureChecks.SupportsStructAggregate; +import org.hibernate.testing.orm.junit.DialectFeatureChecks.SupportsTypedArrays; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +@SessionFactory +@DomainModel(annotatedClasses = { + StructArrayWithNullElementTestDemoTest.Book.class, + StructArrayWithNullElementTestDemoTest.Author.class +}) +@RequiresDialectFeature(feature = SupportsStructAggregate.class) +@RequiresDialectFeature(feature = SupportsTypedArrays.class) +class StructArrayWithNullElementTestDemoTest { + + @Test + void test(SessionFactoryScope scope) { + scope.inTransaction( session -> { + var book = new Book(); + book.id = 1; + book.authors = Arrays.asList( + new Author( "John", "Smith" ), + null + ); + session.persist( book ); + } ); + + scope.inSession( session -> { + final var book = session.find( Book.class, 1 ); + assertEquals( 2, book.authors.size() ); + assertEquals( new Author( "John", "Smith" ), book.authors.get( 0 ) ); + assertNull( book.authors.get( 1 ) ); + } ); + } + + @Entity(name = "Book") + @Table(name = "books") + static class Book { + @Id + int id; + List authors; + } + + @Embeddable + @Struct(name = "Author") + static final class Author { + String firstName; + String lastName; + + public Author() { + } + + public Author(String firstName, String lastName) { + this.firstName = firstName; + this.lastName = lastName; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + Author author = (Author) o; + return Objects.equals(firstName, author.firstName) && Objects.equals(lastName, author.lastName); + } + + @Override + public int hashCode() { + return Objects.hash(firstName, lastName); + } + } +} diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java index dd8e2475d133..1aaca7fa8869 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java @@ -695,6 +695,12 @@ public boolean apply(Dialect dialect) { } } + public static class SupportsTypedArrays implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return dialect.getPreferredSqlTypeCodeForArray() == SqlTypes.ARRAY; + } + } + public static class SupportsUpsertOrMerge implements DialectFeatureCheck { public boolean apply(Dialect dialect) { return !( dialect instanceof DerbyDialect );