From ba985518c77251e5df4df187e5ba58d03c76146d Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Wed, 21 Dec 2022 11:55:38 +0100 Subject: [PATCH] HHH-15898 Add @Instantiator annotation to mark canonical constructor for embeddables --- .../hibernate/annotations/Instantiator.java | 31 ++ .../cfg/AggregateComponentSecondPass.java | 30 +- .../org/hibernate/cfg/AnnotationBinder.java | 26 ++ .../java/org/hibernate/mapping/Component.java | 59 ++- ...EmbeddableInstantiatorPojoIndirecting.java | 84 +++++ .../EmbeddableInstantiatorPojoStandard.java | 9 +- ...beddableInstantiatorRecordIndirecting.java | 19 +- .../EmbeddableRepresentationStrategyPojo.java | 2 +- ...gedTypeRepresentationResolverStandard.java | 13 +- .../StructComponentInstantiatorTest.java | 340 ++++++++++++++++++ .../metadata/ComponentMetadataGenerator.java | 8 + 11 files changed, 577 insertions(+), 44 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/annotations/Instantiator.java create mode 100644 hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableInstantiatorPojoIndirecting.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/component/StructComponentInstantiatorTest.java diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/Instantiator.java b/hibernate-core/src/main/java/org/hibernate/annotations/Instantiator.java new file mode 100644 index 0000000000..eeb10546d2 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/annotations/Instantiator.java @@ -0,0 +1,31 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.annotations; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import org.hibernate.Incubating; + +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Marks the canonical constructor to be used for instantation of an embeddable. + * This will implicitly add a special {@link EmbeddableInstantiator}. + * + * @since 6.2 + */ +@Target({ CONSTRUCTOR }) +@Retention(RUNTIME) +@Incubating +public @interface Instantiator { + /** + * The persistent attribute names the constructor parameters at the respective index assigns the value to. + */ + String[] value(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/AggregateComponentSecondPass.java b/hibernate-core/src/main/java/org/hibernate/cfg/AggregateComponentSecondPass.java index 9a67fc005f..b1ae22c605 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/AggregateComponentSecondPass.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/AggregateComponentSecondPass.java @@ -7,7 +7,6 @@ package org.hibernate.cfg; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.TreeSet; @@ -33,6 +32,7 @@ import org.hibernate.mapping.PersistentClass; import org.hibernate.mapping.Property; import org.hibernate.mapping.UserDefinedType; import org.hibernate.mapping.Value; +import org.hibernate.metamodel.internal.EmbeddableHelper; import org.hibernate.sql.Template; import org.hibernate.type.BasicPluralType; import org.hibernate.type.BasicType; @@ -195,12 +195,20 @@ public class AggregateComponentSecondPass implements SecondPass { } else { final String[] componentNames = ReflectHelper.getRecordComponentNames( componentClass ); - propertyMappingIndex = determinePropertyMappingIndex( componentNames ); + propertyMappingIndex = EmbeddableHelper.determinePropertyMappingIndex( + component.getPropertyNames(), + componentNames + ); } } else { - // At some point we could do some byte code analysis to determine the order based on a constructor - return; + if ( component.getInstantiatorPropertyNames() == null ) { + return; + } + propertyMappingIndex = EmbeddableHelper.determinePropertyMappingIndex( + component.getPropertyNames(), + component.getInstantiatorPropertyNames() + ); } final ArrayList orderedColumns = new ArrayList<>( userDefinedType.getColumnSpan() ); final List properties = component.getProperties(); @@ -210,8 +218,7 @@ public class AggregateComponentSecondPass implements SecondPass { } } else { - for ( int newIndex = 0; newIndex < propertyMappingIndex.length; newIndex++ ) { - final int propertyIndex = propertyMappingIndex[newIndex]; + for ( final int propertyIndex : propertyMappingIndex ) { addColumns( orderedColumns, properties.get( propertyIndex ).getValue() ); } } @@ -235,17 +242,6 @@ public class AggregateComponentSecondPass implements SecondPass { } } - private static int[] determinePropertyMappingIndex(String[] componentNames) { - final String[] sortedComponentNames = componentNames.clone(); - final int[] index = new int[componentNames.length]; - Arrays.sort( sortedComponentNames ); - for ( int i = 0; i < componentNames.length; i++ ) { - final int newIndex = Arrays.binarySearch( sortedComponentNames, componentNames[i] ); - index[newIndex] = i; - } - return index; - } - private void validateSupportedColumnTypes(String basePath, Component component) { for ( Property property : component.getProperties() ) { final Value value = property.getValue(); diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/AnnotationBinder.java b/hibernate-core/src/main/java/org/hibernate/cfg/AnnotationBinder.java index 690fed55b5..741cc0e744 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/AnnotationBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/AnnotationBinder.java @@ -7,6 +7,7 @@ package org.hibernate.cfg; import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.time.OffsetDateTime; @@ -42,6 +43,7 @@ import org.hibernate.annotations.GenericGenerators; import org.hibernate.annotations.IdGeneratorType; import org.hibernate.annotations.Imported; import org.hibernate.annotations.Index; +import org.hibernate.annotations.Instantiator; import org.hibernate.annotations.JavaTypeRegistration; import org.hibernate.annotations.JavaTypeRegistrations; import org.hibernate.annotations.JdbcTypeRegistration; @@ -2330,6 +2332,10 @@ public final class AnnotationBinder { component.setComponentClassName( inferredData.getClassOrElementName() ); } component.setCustomInstantiator( customInstantiatorImpl ); + final Constructor constructor = resolveInstantiator( inferredData.getClassOrElement(), context ); + if ( constructor != null ) { + component.setInstantiator( constructor, constructor.getAnnotation( Instantiator.class ).value() ); + } return component; } @@ -2578,4 +2584,24 @@ public final class AnnotationBinder { } } } + + public static Constructor resolveInstantiator(XClass embeddableClass, MetadataBuildingContext buildingContext) { + if ( embeddableClass != null ) { + final Constructor[] declaredConstructors = buildingContext.getBootstrapContext().getReflectionManager() + .toClass( embeddableClass ) + .getDeclaredConstructors(); + Constructor constructor = null; + for ( Constructor declaredConstructor : declaredConstructors ) { + if ( declaredConstructor.isAnnotationPresent( Instantiator.class ) ) { + if ( constructor != null ) { + throw new MappingException( + "Multiple constructors annotated with @Instantiator but only one constructor can be the canonical constructor in class: " + embeddableClass.getName() ); + } + constructor = declaredConstructor; + } + } + return constructor; + } + return null; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/Component.java b/hibernate-core/src/main/java/org/hibernate/mapping/Component.java index 8dca365c07..a8a8b8f00a 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/Component.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/Component.java @@ -6,6 +6,7 @@ */ package org.hibernate.mapping; +import java.lang.reflect.Constructor; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; @@ -17,6 +18,7 @@ import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; +import org.hibernate.Internal; import org.hibernate.MappingException; import org.hibernate.Remove; import org.hibernate.boot.model.relational.Database; @@ -27,12 +29,14 @@ import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; import org.hibernate.boot.registry.classloading.spi.ClassLoadingException; import org.hibernate.boot.spi.MetadataBuildingContext; import org.hibernate.dialect.Dialect; +import org.hibernate.engine.spi.Mapping; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.id.CompositeNestedGeneratedValueGenerator; import org.hibernate.id.IdentifierGenerator; import org.hibernate.id.factory.IdentifierGeneratorFactory; import org.hibernate.internal.util.ReflectHelper; import org.hibernate.internal.util.collections.ArrayHelper; +import org.hibernate.internal.util.collections.CollectionHelper; import org.hibernate.internal.util.collections.JoinedIterator; import org.hibernate.metamodel.spi.EmbeddableInstantiator; import org.hibernate.property.access.spi.Setter; @@ -69,6 +73,8 @@ public class Component extends SimpleValue implements MetaAttributable, Sortable private Map metaAttributes; private Class customInstantiator; + private Constructor instantiator; + private String[] instantiatorPropertyNames; // cache the status of the type private volatile Type type; @@ -614,6 +620,15 @@ public class Component extends SimpleValue implements MetaAttributable, Sortable } } + @Internal + public String[] getPropertyNames() { + final String[] propertyNames = new String[properties.size()]; + for ( int i = 0; i < properties.size(); i++ ) { + propertyNames[i] = properties.get( i ).getName(); + } + return propertyNames; + } + public static class StandardGenerationContextLocator implements CompositeNestedGeneratedValueGenerator.GenerationContextLocator { private final String entityName; @@ -668,6 +683,35 @@ public class Component extends SimpleValue implements MetaAttributable, Sortable getType(); } + @Override + public boolean isValid(Mapping mapping) throws MappingException { + if ( !super.isValid( mapping ) ) { + return false; + } + if ( instantiatorPropertyNames != null ) { + if ( instantiatorPropertyNames.length < properties.size() ) { + throw new MappingException( "component type [" + componentClassName + "] specifies " + instantiatorPropertyNames.length + " properties for the instantiator but has " + properties.size() + " properties" ); + } + final HashSet assignedPropertyNames = CollectionHelper.setOfSize( properties.size() ); + for ( String instantiatorPropertyName : instantiatorPropertyNames ) { + if ( getProperty( instantiatorPropertyName ) == null ) { + throw new MappingException( "could not find property [" + instantiatorPropertyName + "] defined in the @Instantiator withing component [" + componentClassName + "]" ); + } + assignedPropertyNames.add( instantiatorPropertyName ); + } + if ( assignedPropertyNames.size() != properties.size() ) { + final ArrayList missingProperties = new ArrayList<>(); + for ( Property property : properties ) { + if ( !assignedPropertyNames.contains( property.getName() ) ) { + missingProperties.add( property.getName() ); + } + } + throw new MappingException( "component type [" + componentClassName + "] has " + properties.size() + " properties but the instantiator only assigns " + assignedPropertyNames.size() + " properties. missing properties: " + missingProperties ); + } + } + return true; + } + @Override public boolean isSorted() { return originalPropertyOrder != ArrayHelper.EMPTY_INT_ARRAY; @@ -678,7 +722,6 @@ public class Component extends SimpleValue implements MetaAttributable, Sortable return sortProperties( false ); } - private int[] sortProperties(boolean forceRetainOriginalOrder) { if ( originalPropertyOrder != ArrayHelper.EMPTY_INT_ARRAY ) { return originalPropertyOrder; @@ -750,4 +793,18 @@ public class Component extends SimpleValue implements MetaAttributable, Sortable public void setCustomInstantiator(Class customInstantiator) { this.customInstantiator = customInstantiator; } + + public Constructor getInstantiator() { + return instantiator; + } + + public String[] getInstantiatorPropertyNames() { + return instantiatorPropertyNames; + } + + public void setInstantiator(Constructor instantiator, String[] instantiatorPropertyNames) { + this.instantiator = instantiator; + this.instantiatorPropertyNames = instantiatorPropertyNames; + } + } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableInstantiatorPojoIndirecting.java b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableInstantiatorPojoIndirecting.java new file mode 100644 index 0000000000..bbfa7f9c31 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableInstantiatorPojoIndirecting.java @@ -0,0 +1,84 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.metamodel.internal; + +import java.lang.reflect.Constructor; + +import org.hibernate.InstantiationException; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.metamodel.spi.ValueAccess; + +/** + * Support for instantiating embeddables as POJO representation through a constructor + */ +public class EmbeddableInstantiatorPojoIndirecting extends AbstractPojoInstantiator implements StandardEmbeddableInstantiator { + protected final Constructor constructor; + protected final int[] index; + + protected EmbeddableInstantiatorPojoIndirecting(Constructor constructor, int[] index) { + super( constructor.getDeclaringClass() ); + this.constructor = constructor; + this.index = index; + } + + public static EmbeddableInstantiatorPojoIndirecting of( + String[] propertyNames, + Constructor constructor, + String[] componentNames) { + if ( componentNames == null ) { + throw new IllegalArgumentException( "Can't determine field assignment for constructor: " + constructor ); + } + final int[] index = new int[componentNames.length]; + if ( EmbeddableHelper.resolveIndex( propertyNames, componentNames, index ) ) { + return new EmbeddableInstantiatorPojoIndirecting.EmbeddableInstantiatorPojoIndirectingWithGap( constructor, index ); + } + else { + return new EmbeddableInstantiatorPojoIndirecting( constructor, index ); + } + } + + @Override + public Object instantiate(ValueAccess valuesAccess, SessionFactoryImplementor sessionFactory) { + try { + final Object[] originalValues = valuesAccess.getValues(); + final Object[] values = new Object[originalValues.length]; + for ( int i = 0; i < values.length; i++ ) { + values[i] = originalValues[index[i]]; + } + return constructor.newInstance( values ); + } + catch ( Exception e ) { + throw new InstantiationException( "Could not instantiate entity: ", getMappedPojoClass(), e ); + } + } + + // Handles gaps, by leaving the value null for that index + private static class EmbeddableInstantiatorPojoIndirectingWithGap extends EmbeddableInstantiatorPojoIndirecting { + + public EmbeddableInstantiatorPojoIndirectingWithGap(Constructor constructor, int[] index) { + super( constructor, index ); + } + + @Override + public Object instantiate(ValueAccess valuesAccess, SessionFactoryImplementor sessionFactory) { + try { + final Object[] originalValues = valuesAccess.getValues(); + final Object[] values = new Object[index.length]; + for ( int i = 0; i < values.length; i++ ) { + final int index = this.index[i]; + if ( index >= 0 ) { + values[i] = originalValues[index]; + } + } + return constructor.newInstance( values ); + } + catch ( Exception e ) { + throw new InstantiationException( "Could not instantiate entity: ", getMappedPojoClass(), e ); + } + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableInstantiatorPojoStandard.java b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableInstantiatorPojoStandard.java index 9af4e30ee2..795648f8ca 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableInstantiatorPojoStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableInstantiatorPojoStandard.java @@ -26,7 +26,6 @@ public class EmbeddableInstantiatorPojoStandard extends AbstractPojoInstantiator private static final CoreMessageLogger LOG = CoreLogging.messageLogger( PojoInstantiatorImpl.class ); private final Supplier embeddableMappingAccess; - private final boolean constructorInjection = false; private final Constructor constructor; public EmbeddableInstantiatorPojoStandard(JavaType javaType, Supplier embeddableMappingAccess) { @@ -38,7 +37,7 @@ public class EmbeddableInstantiatorPojoStandard extends AbstractPojoInstantiator protected static Constructor resolveConstructor(Class mappedPojoClass) { try { - return ReflectHelper.getDefaultConstructor( mappedPojoClass); + return ReflectHelper.getDefaultConstructor( mappedPojoClass ); } catch ( PropertyNotFoundException e ) { LOG.noDefaultConstructor( mappedPojoClass.getName() ); @@ -60,11 +59,7 @@ public class EmbeddableInstantiatorPojoStandard extends AbstractPojoInstantiator } try { - if ( constructorInjection ) { - return constructor.newInstance( valuesAccess.getValues() ); - } - - Object[] values = valuesAccess == null ? null : valuesAccess.getValues(); + final Object[] values = valuesAccess == null ? null : valuesAccess.getValues(); final Object instance = constructor.newInstance(); if ( values != null ) { // At this point, createEmptyCompositesEnabled is always true. diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableInstantiatorRecordIndirecting.java b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableInstantiatorRecordIndirecting.java index 75e023d961..b0fb040c39 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableInstantiatorRecordIndirecting.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableInstantiatorRecordIndirecting.java @@ -6,8 +6,6 @@ */ package org.hibernate.metamodel.internal; -import java.util.Arrays; - import org.hibernate.InstantiationException; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.internal.util.ReflectHelper; @@ -25,10 +23,10 @@ public class EmbeddableInstantiatorRecordIndirecting extends EmbeddableInstantia this.index = index; } - public static EmbeddableInstantiatorRecordIndirecting of(Class javaType) { + public static EmbeddableInstantiatorRecordIndirecting of(Class javaType, String[] propertyNames) { final String[] componentNames = ReflectHelper.getRecordComponentNames( javaType ); final int[] index = new int[componentNames.length]; - if ( resolveIndex( componentNames, index ) ) { + if ( EmbeddableHelper.resolveIndex( propertyNames, componentNames, index ) ) { return new EmbeddableInstantiatorRecordIndirectingWithGap( javaType, index ); } else { @@ -36,19 +34,6 @@ public class EmbeddableInstantiatorRecordIndirecting extends EmbeddableInstantia } } - private static boolean resolveIndex(String[] componentNames, int[] index) { - final String[] sortedComponentNames = componentNames.clone(); - Arrays.sort( sortedComponentNames ); - boolean hasGaps = false; - for ( int i = 0; i < componentNames.length; i++ ) { - final int newIndex = Arrays.binarySearch( sortedComponentNames, componentNames[i] ); - index[i] = newIndex; - hasGaps = hasGaps || newIndex < 0; - } - - return hasGaps; - } - @Override public Object instantiate(ValueAccess valuesAccess, SessionFactoryImplementor sessionFactory) { if ( constructor == null ) { diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableRepresentationStrategyPojo.java b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableRepresentationStrategyPojo.java index 60b039d89a..392cec8d10 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableRepresentationStrategyPojo.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableRepresentationStrategyPojo.java @@ -186,7 +186,7 @@ public class EmbeddableRepresentationStrategyPojo extends AbstractEmbeddableRepr return null; } - if ( hasCustomAccessors() || bootDescriptor.getCustomInstantiator() != null ) { + if ( hasCustomAccessors() || bootDescriptor.getCustomInstantiator() != null || bootDescriptor.getInstantiator() != null ) { return null; } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/ManagedTypeRepresentationResolverStandard.java b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/ManagedTypeRepresentationResolverStandard.java index ba72f8c82a..1b7ac85b17 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/ManagedTypeRepresentationResolverStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/ManagedTypeRepresentationResolverStandard.java @@ -110,9 +110,20 @@ public class ManagedTypeRepresentationResolverStandard implements ManagedTypeRep customInstantiator = new EmbeddableInstantiatorRecordStandard( bootDescriptor.getComponentClass() ); } else { - customInstantiator = EmbeddableInstantiatorRecordIndirecting.of( bootDescriptor.getComponentClass() ); + customInstantiator = EmbeddableInstantiatorRecordIndirecting.of( + bootDescriptor.getComponentClass(), + bootDescriptor.getPropertyNames() + ); } } + else if ( bootDescriptor.getInstantiator() != null ) { + bootDescriptor.sortProperties(); + customInstantiator = EmbeddableInstantiatorPojoIndirecting.of( + bootDescriptor.getPropertyNames(), + bootDescriptor.getInstantiator(), + bootDescriptor.getInstantiatorPropertyNames() + ); + } else { customInstantiator = null; } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/component/StructComponentInstantiatorTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/component/StructComponentInstantiatorTest.java new file mode 100644 index 0000000000..fc036aa0b6 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/component/StructComponentInstantiatorTest.java @@ -0,0 +1,340 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.component; + +import java.util.List; +import java.util.Objects; + +import org.hibernate.annotations.Instantiator; +import org.hibernate.annotations.Struct; +import org.hibernate.boot.registry.StandardServiceRegistry; +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.dialect.OracleDialect; +import org.hibernate.dialect.PostgreSQLDialect; +import org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl; + +import org.hibernate.testing.orm.junit.BaseSessionFactoryFunctionalTest; +import org.hibernate.testing.orm.junit.RequiresDialect; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Tuple; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; + +@RequiresDialect( PostgreSQLDialect.class ) +@RequiresDialect( OracleDialect.class ) +public class StructComponentInstantiatorTest extends BaseSessionFactoryFunctionalTest { + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { + RecordStructHolder.class + }; + } + + @Override + public StandardServiceRegistry produceServiceRegistry(StandardServiceRegistryBuilder ssrBuilder) { + // Make sure this stuff runs on a dedicated connection pool, + // otherwise we might run into ORA-21700: object does not exist or is marked for delete + // because the JDBC connection or database session caches something that should have been invalidated + ssrBuilder.applySetting( AvailableSettings.CONNECTION_PROVIDER, DriverManagerConnectionProviderImpl.class.getName() ); + return super.produceServiceRegistry( ssrBuilder ); + } + + @BeforeEach + public void setUp() { + inTransaction( + session -> { + session.persist( new RecordStructHolder( 1L, Point.createAggregate1() ) ); + session.persist( new RecordStructHolder( 2L, Point.createAggregate2() ) ); + } + ); + } + + @AfterEach + protected void cleanupTest() { + inTransaction( + session -> { + session.createQuery( "delete from RecordStructHolder h" ).executeUpdate(); + } + ); + } + + @Test + public void testUpdate() { + sessionFactoryScope().inTransaction( + entityManager -> { + RecordStructHolder RecordStructHolder = entityManager.find( RecordStructHolder.class, 1L ); + RecordStructHolder.setThePoint( Point.createAggregate2() ); + entityManager.flush(); + entityManager.clear(); + assertStructEquals( Point.createAggregate2(), entityManager.find( RecordStructHolder.class, 1L ).getThePoint() ); + } + ); + } + + @Test + public void testFetch() { + sessionFactoryScope().inSession( + entityManager -> { + List RecordStructHolders = entityManager.createQuery( "from RecordStructHolder b where b.id = 1", RecordStructHolder.class ).getResultList(); + assertEquals( 1, RecordStructHolders.size() ); + assertEquals( 1L, RecordStructHolders.get( 0 ).getId() ); + assertStructEquals( Point.createAggregate1(), RecordStructHolders.get( 0 ).getThePoint() ); + } + ); + } + + @Test + public void testFetchNull() { + sessionFactoryScope().inSession( + entityManager -> { + List RecordStructHolders = entityManager.createQuery( "from RecordStructHolder b where b.id = 2", RecordStructHolder.class ).getResultList(); + assertEquals( 1, RecordStructHolders.size() ); + assertEquals( 2L, RecordStructHolders.get( 0 ).getId() ); + assertStructEquals( Point.createAggregate2(), RecordStructHolders.get( 0 ).getThePoint() ); + } + ); + } + + @Test + public void testDomainResult() { + sessionFactoryScope().inSession( + entityManager -> { + List structs = entityManager.createQuery( "select b.thePoint from RecordStructHolder b where b.id = 1", Point.class ).getResultList(); + assertEquals( 1, structs.size() ); + assertStructEquals( Point.createAggregate1(), structs.get( 0 ) ); + } + ); + } + + @Test + public void testSelectionItems() { + sessionFactoryScope().inSession( + entityManager -> { + List tuples = entityManager.createQuery( + "select " + + "b.thePoint.x," + + "b.thePoint.y," + + "b.thePoint.z " + + "from RecordStructHolder b where b.id = 1", + Tuple.class + ).getResultList(); + assertEquals( 1, tuples.size() ); + final Tuple tuple = tuples.get( 0 ); + assertStructEquals( + Point.createAggregate1(), + new Point( + tuple.get( 1, String.class ), + tuple.get( 2, long.class ), + tuple.get( 0, int.class ) + ) + ); + } + ); + } + + @Test + public void testDeleteWhere() { + sessionFactoryScope().inTransaction( + entityManager -> { + entityManager.createQuery( "delete RecordStructHolder b where b.thePoint is not null" ).executeUpdate(); + assertNull( entityManager.find( RecordStructHolder.class, 1L ) ); + } + ); + } + + @Test + public void testUpdateAggregate() { + sessionFactoryScope().inTransaction( + entityManager -> { + entityManager.createQuery( "update RecordStructHolder b set b.thePoint = null" ).executeUpdate(); + assertNull( entityManager.find( RecordStructHolder.class, 1L ).getThePoint() ); + } + ); + } + + @Test + public void testUpdateAggregateMember() { + sessionFactoryScope().inTransaction( + entityManager -> { + entityManager.createQuery( "update RecordStructHolder b set b.thePoint.x = null" ).executeUpdate(); + Point struct = Point.createAggregate1().withX( null ); + assertStructEquals( struct, entityManager.find( RecordStructHolder.class, 1L ).getThePoint() ); + } + ); + } + + @Test + public void testUpdateMultipleAggregateMembers() { + sessionFactoryScope().inTransaction( + entityManager -> { + entityManager.createQuery( "update RecordStructHolder b set b.thePoint.y = null, b.thePoint.z = 0" ).executeUpdate(); + Point struct = Point.createAggregate1().withY( null ).withZ( 0 ); + assertStructEquals( struct, entityManager.find( RecordStructHolder.class, 1L ).getThePoint() ); + } + ); + } + + @Test + public void testUpdateAllAggregateMembers() { + sessionFactoryScope().inTransaction( + entityManager -> { + Point struct = Point.createAggregate1(); + entityManager.createQuery( + "update RecordStructHolder b set " + + "b.thePoint.x = :x," + + "b.thePoint.y = :y," + + "b.thePoint.z = :z " + + "where b.id = 2" + ) + .setParameter( "x", struct.getX() ) + .setParameter( "y", struct.getY() ) + .setParameter( "z", struct.getZ() ) + .executeUpdate(); + assertStructEquals( Point.createAggregate1(), entityManager.find( RecordStructHolder.class, 2L ).getThePoint() ); + } + ); + } + + @Test + public void testNativeQuery() { + sessionFactoryScope().inTransaction( + entityManager -> { + //noinspection unchecked + List resultList = entityManager.createNativeQuery( + "select b.thePoint from RecordStructHolder b where b.id = 1", + Object.class + ) + .getResultList(); + assertEquals( 1, resultList.size() ); + assertInstanceOf( Point.class, resultList.get( 0 ) ); + Point struct = (Point) resultList.get( 0 ); + assertStructEquals( Point.createAggregate1(), struct ); + } + ); + } + + private void assertStructEquals(Point point1, Point point2) { + assertEquals( point1.getX(), point2.getX() ); + assertEquals( point1.getY(), point2.getY() ); + assertEquals( point1.getZ(), point2.getZ() ); + } + + @Entity(name = "RecordStructHolder") + public static class RecordStructHolder { + + @Id + private Long id; + @Struct(name = "my_point_type") + private Point thePoint; + + public RecordStructHolder() { + } + + public RecordStructHolder(Long id, Point thePoint) { + this.id = id; + this.thePoint = thePoint; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Point getThePoint() { + return thePoint; + } + + public void setThePoint(Point aggregate) { + this.thePoint = aggregate; + } + + } + + @Embeddable + public static class Point { + + private final String y; + private final long z; + private final Integer x; + + @Instantiator({"y","z","x"}) + public Point(String y, long z, Integer x) { + this.y = y; + this.x = x; + this.z = z; + } + + public String getY() { + return y; + } + + public Integer getX() { + return x; + } + + public long getZ() { + return z; + } + + public Point withX(Integer x) { + return new Point( y, z, x ); + } + public Point withY(String y) { + return new Point( y, z, x ); + } + public Point withZ(long z) { + return new Point( y, z, x ); + } + public static Point createAggregate1() { + return new Point( "1", -100, 10 ); + } + public static Point createAggregate2() { + return new Point( "20", -200, 2 ); + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + + Point point = (Point) o; + + if ( !Objects.equals( y, point.y ) ) { + return false; + } + if ( !Objects.equals( x, point.x ) ) { + return false; + } + return Objects.equals( z, point.z ); + } + + @Override + public int hashCode() { + int result = y != null ? y.hashCode() : 0; + result = 31 * result + (int) ( z ^ ( z >>> 32 ) ); + result = 31 * result + ( x != null ? x.hashCode() : 0 ); + return result; + } + } +} diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/ComponentMetadataGenerator.java b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/ComponentMetadataGenerator.java index 789b1fe5ca..4cf51c785f 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/ComponentMetadataGenerator.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/ComponentMetadataGenerator.java @@ -19,6 +19,7 @@ import org.hibernate.mapping.Component; import org.hibernate.mapping.Property; import org.hibernate.mapping.Value; import org.hibernate.metamodel.internal.EmbeddableCompositeUserTypeInstantiator; +import org.hibernate.metamodel.internal.EmbeddableInstantiatorPojoIndirecting; import org.hibernate.metamodel.spi.EmbeddableInstantiator; import org.hibernate.resource.beans.spi.ManagedBeanRegistry; import org.hibernate.usertype.CompositeUserType; @@ -69,6 +70,13 @@ public final class ComponentMetadataGenerator extends AbstractMetadataGenerator .getBeanInstance(); instantiator = new EmbeddableCompositeUserTypeInstantiator( compositeUserType ); } + else if ( propComponent.getInstantiator() != null ) { + instantiator = EmbeddableInstantiatorPojoIndirecting.of( + propComponent.getPropertyNames(), + propComponent.getInstantiator(), + propComponent.getInstantiatorPropertyNames() + ); + } else { instantiator = null; }