diff --git a/documentation/src/test/java/org/hibernate/userguide/mapping/basic/ColumnTransformerTest.java b/documentation/src/test/java/org/hibernate/userguide/mapping/basic/ColumnTransformerTest.java index 45967ead9a..4d7006ee2a 100644 --- a/documentation/src/test/java/org/hibernate/userguide/mapping/basic/ColumnTransformerTest.java +++ b/documentation/src/test/java/org/hibernate/userguide/mapping/basic/ColumnTransformerTest.java @@ -11,12 +11,16 @@ import java.util.Currency; import java.util.Locale; import org.hibernate.annotations.ColumnTransformer; +import org.hibernate.annotations.CompositeType; import org.hibernate.dialect.H2Dialect; import org.hibernate.orm.test.jpa.BaseEntityManagerFunctionalTestCase; import org.hibernate.testing.RequiresDialect; import org.junit.Test; +import jakarta.persistence.AttributeOverride; +import jakarta.persistence.AttributeOverrides; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Id; @@ -43,15 +47,14 @@ public class ColumnTransformerTest extends BaseEntityManagerFunctionalTestCase { //tag::basic-datetime-temporal-date-persist-example[] Savings savings = new Savings(); savings.setId(1L); - savings.setCurrency(Currency.getInstance(Locale.US)); - savings.setAmount(BigDecimal.TEN); + savings.setWallet(new MonetaryAmount(BigDecimal.TEN, Currency.getInstance(Locale.US))); entityManager.persist(savings); }); doInJPA(this::entityManagerFactory, entityManager -> { Savings savings = entityManager.find(Savings.class, 1L); - assertEquals(10, savings.getAmount().intValue()); - assertEquals(Currency.getInstance(Locale.US), savings.getCurrency()); + assertEquals(10, savings.getWallet().getAmount().intValue()); + assertEquals(Currency.getInstance(Locale.US), savings.getWallet().getCurrency()); }); //end::mapping-column-read-and-write-composite-type-persistence-example[] } @@ -63,12 +66,17 @@ public class ColumnTransformerTest extends BaseEntityManagerFunctionalTestCase { @Id private Long id; + @CompositeType(MonetaryAmountUserType.class) + @AttributeOverrides({ + @AttributeOverride(name = "amount", column = @Column(name = "money")), + @AttributeOverride(name = "currency", column = @Column(name = "currency")) + }) @ColumnTransformer( - read = "amount / 100", + forColumn = "money", + read = "money / 100", write = "? * 100" ) - private BigDecimal amount; - private Currency currency; + private MonetaryAmount wallet; //Getters and setters omitted for brevity @@ -81,20 +89,12 @@ public class ColumnTransformerTest extends BaseEntityManagerFunctionalTestCase { this.id = id; } - public BigDecimal getAmount() { - return amount; + public MonetaryAmount getWallet() { + return wallet; } - public void setAmount(BigDecimal amount) { - this.amount = amount; - } - - public Currency getCurrency() { - return currency; - } - - public void setCurrency(Currency currency) { - this.currency = currency; + public void setWallet(MonetaryAmount wallet) { + this.wallet = wallet; } diff --git a/documentation/src/test/java/org/hibernate/userguide/mapping/basic/MonetaryAmountUserType.java b/documentation/src/test/java/org/hibernate/userguide/mapping/basic/MonetaryAmountUserType.java new file mode 100644 index 0000000000..32966c4a7e --- /dev/null +++ b/documentation/src/test/java/org/hibernate/userguide/mapping/basic/MonetaryAmountUserType.java @@ -0,0 +1,96 @@ +/* + * 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.userguide.mapping.basic; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Currency; +import java.util.function.Supplier; + +import org.hibernate.HibernateException; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.usertype.CompositeUserType; + +/** + * @author Emmanuel Bernard + */ +public class MonetaryAmountUserType implements CompositeUserType { + + @Override + public Object getPropertyValue(MonetaryAmount component, int property) throws HibernateException { + switch ( property ) { + case 0: + return component.getAmount(); + case 1: + return component.getCurrency(); + } + throw new HibernateException( "Illegal property index: " + property ); + } + + @Override + public MonetaryAmount instantiate(Supplier valueSupplier, SessionFactoryImplementor sessionFactory) { + final Object[] values = valueSupplier.get(); + return new MonetaryAmount( (BigDecimal) values[0], (Currency) values[1] ); + } + + @Override + public Class embeddable() { + return MonetaryAmountEmbeddable.class; + } + + @Override + public Class returnedClass() { + return MonetaryAmount.class; + } + + @Override + public boolean isMutable() { + return true; + } + + @Override + public Object deepCopy(Object value) { + MonetaryAmount ma = (MonetaryAmount) value; + return new MonetaryAmount( ma.getAmount(), ma.getCurrency() ); + } + + @Override + public boolean equals(Object x, Object y) { + if ( x == y ) { + return true; + } + if ( x == null || y == null ) { + return false; + } + return x.equals( y ); + } + + @Override + public Serializable disassemble(Object value) throws HibernateException { + return (Serializable) deepCopy( value ); + } + + @Override + public Object assemble(Serializable cached, Object owner) throws HibernateException { + return deepCopy( cached ); + } + + @Override + public Object replace(Object original, Object target, Object owner) throws HibernateException { + return deepCopy( original ); //TODO: improve + } + + @Override + public int hashCode(Object x) throws HibernateException { + return x.hashCode(); + } + + public static class MonetaryAmountEmbeddable { + private BigDecimal amount; + private Currency currency; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/CompositeType.java b/hibernate-core/src/main/java/org/hibernate/annotations/CompositeType.java new file mode 100644 index 0000000000..28c33c430e --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/annotations/CompositeType.java @@ -0,0 +1,28 @@ +/* + * 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 org.hibernate.usertype.CompositeUserType; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Applies a custom {@link CompositeUserType} for the attribute mapping. + */ +@java.lang.annotation.Target({METHOD, FIELD}) +@Retention(RUNTIME) +public @interface CompositeType { + + /** + * The custom type implementor class + */ + Class> value(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/CompositeTypeRegistration.java b/hibernate-core/src/main/java/org/hibernate/annotations/CompositeTypeRegistration.java new file mode 100644 index 0000000000..b7e8f52b7f --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/annotations/CompositeTypeRegistration.java @@ -0,0 +1,32 @@ +/* + * 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.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import org.hibernate.usertype.CompositeUserType; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.PACKAGE; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Registers a custom composite user type implementation to be used + * for all references to a particular {@link jakarta.persistence.Embeddable}. + *

+ * May be overridden for a specific embedded using {@link org.hibernate.annotations.CompositeType} + */ +@Target( {TYPE, ANNOTATION_TYPE, PACKAGE} ) +@Retention( RUNTIME ) +@Repeatable( CompositeTypeRegistrations.class ) +public @interface CompositeTypeRegistration { + Class embeddableClass(); + Class> userType(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/CompositeTypeRegistrations.java b/hibernate-core/src/main/java/org/hibernate/annotations/CompositeTypeRegistrations.java new file mode 100644 index 0000000000..78eb516792 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/annotations/CompositeTypeRegistrations.java @@ -0,0 +1,26 @@ +/* + * 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 static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.PACKAGE; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Grouping of {@link CompositeTypeRegistration} + * + * @author Steve Ebersole + */ +@Target( {TYPE, ANNOTATION_TYPE, PACKAGE} ) +@Retention( RUNTIME ) +public @interface CompositeTypeRegistrations { + CompositeTypeRegistration[] value(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/MapKeyCustomCompositeType.java b/hibernate-core/src/main/java/org/hibernate/annotations/MapKeyCustomCompositeType.java new file mode 100644 index 0000000000..5d09f92c19 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/annotations/MapKeyCustomCompositeType.java @@ -0,0 +1,23 @@ +/* + * 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 org.hibernate.usertype.CompositeUserType; + +/** + * Form of {@link CompositeType} for use with map-keys + * + * @since 6.0 + */ +public @interface MapKeyCustomCompositeType { + /** + * The custom type implementor class + * + * @see CompositeType#value + */ + Class> value(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/boot/internal/InFlightMetadataCollectorImpl.java b/hibernate-core/src/main/java/org/hibernate/boot/internal/InFlightMetadataCollectorImpl.java index 482c568f54..dd3f543de6 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/internal/InFlightMetadataCollectorImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/internal/InFlightMetadataCollectorImpl.java @@ -104,6 +104,7 @@ import org.hibernate.query.sqm.function.SqmFunctionDescriptor; import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.spi.TypeConfiguration; +import org.hibernate.usertype.CompositeUserType; import jakarta.persistence.AttributeConverter; import jakarta.persistence.Embeddable; @@ -410,6 +411,25 @@ public class InFlightMetadataCollectorImpl implements InFlightMetadataCollector return registeredInstantiators.get( embeddableType ); } + private Map, Class>> registeredCompositeUserTypes; + + @Override + public void registerCompositeUserType(Class embeddableType, Class> userType) { + if ( registeredCompositeUserTypes == null ) { + registeredCompositeUserTypes = new HashMap<>(); + } + registeredCompositeUserTypes.put( embeddableType, userType ); + } + + @Override + public Class> findRegisteredCompositeUserType(Class embeddableType) { + if ( registeredCompositeUserTypes == null ) { + return null; + } + + return registeredCompositeUserTypes.get( embeddableType ); + } + private Map collectionTypeRegistrations; diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/hbm/ModelBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/hbm/ModelBinder.java index 626d70e0e8..b4b25b967d 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/hbm/ModelBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/hbm/ModelBinder.java @@ -153,6 +153,7 @@ import org.hibernate.type.BasicType; import org.hibernate.type.CustomType; import org.hibernate.type.ForeignKeyDirection; import org.hibernate.type.StandardBasicTypes; +import org.hibernate.usertype.CompositeUserType; import org.hibernate.usertype.ParameterizedType; import org.hibernate.usertype.UserType; @@ -2636,6 +2637,22 @@ public class ModelBinder { else { log.debugf( "Binding component [%s]", role ); if ( StringHelper.isNotEmpty( explicitComponentClassName ) ) { + try { + final Class componentClass = sourceDocument.getBootstrapContext().getClassLoaderAccess() + .classForName( explicitComponentClassName ); + if ( CompositeUserType.class.isAssignableFrom( componentClass ) ) { + componentBinding.setTypeName( explicitComponentClassName ); + CompositeUserType compositeUserType = (CompositeUserType) sourceDocument.getBootstrapContext() + .getServiceRegistry() + .getService( ManagedBeanRegistry.class ) + .getBean( componentClass ) + .getBeanInstance(); + explicitComponentClassName = compositeUserType.embeddable().getName(); + } + } + catch (ClassLoadingException ex) { + log.debugf( ex, "Could load component class [%s]", explicitComponentClassName ); + } log.debugf( "Binding component [%s] to explicitly specified class", role, explicitComponentClassName ); componentBinding.setComponentClassName( explicitComponentClassName ); } diff --git a/hibernate-core/src/main/java/org/hibernate/boot/query/HbmResultSetMappingDescriptor.java b/hibernate-core/src/main/java/org/hibernate/boot/query/HbmResultSetMappingDescriptor.java index 16a62d464e..b3f0782864 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/query/HbmResultSetMappingDescriptor.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/query/HbmResultSetMappingDescriptor.java @@ -653,11 +653,11 @@ public class HbmResultSetMappingDescriptor implements NamedResultSetMappingDescr final FetchParentMemento fetchParentMemento = parent.resolveParentMemento( resolutionContext ); - NavigablePath navigablePath = fetchParentMemento.getNavigablePath().append( propertyPathParts[ 0 ] ); Fetchable fetchable = (Fetchable) fetchParentMemento.getFetchableContainer().findSubPart( propertyPathParts[ 0 ], null ); + NavigablePath navigablePath = fetchParentMemento.getNavigablePath().append( fetchable.getFetchableName() ); for ( int i = 1; i < propertyPathParts.length; i++ ) { if ( ! ( fetchable instanceof FetchableContainer ) ) { @@ -666,8 +666,8 @@ public class HbmResultSetMappingDescriptor implements NamedResultSetMappingDescr + " did not reference FetchableContainer" ); } - navigablePath = navigablePath.append( propertyPathParts[ i ] ); fetchable = (Fetchable) ( (FetchableContainer) fetchable ).findSubPart( propertyPathParts[i], null ); + navigablePath = navigablePath.append( fetchable.getFetchableName() ); } if ( fetchable instanceof BasicValuedModelPart ) { diff --git a/hibernate-core/src/main/java/org/hibernate/boot/spi/InFlightMetadataCollector.java b/hibernate-core/src/main/java/org/hibernate/boot/spi/InFlightMetadataCollector.java index ad9ab17dc6..6d39dfe3b7 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/spi/InFlightMetadataCollector.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/spi/InFlightMetadataCollector.java @@ -7,6 +7,7 @@ package org.hibernate.boot.spi; import java.io.Serializable; +import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -53,6 +54,7 @@ import org.hibernate.metamodel.CollectionClassification; import org.hibernate.metamodel.spi.EmbeddableInstantiator; import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.usertype.CompositeUserType; import org.hibernate.usertype.UserCollectionType; import jakarta.persistence.AttributeConverter; @@ -312,6 +314,9 @@ public interface InFlightMetadataCollector extends Mapping, MetadataImplementor void registerEmbeddableInstantiator(Class embeddableType, Class instantiator); Class findRegisteredEmbeddableInstantiator(Class embeddableType); + void registerCompositeUserType(Class embeddableType, Class> userType); + Class> findRegisteredCompositeUserType(Class embeddableType); + void addCollectionTypeRegistration(CollectionTypeRegistration registrationAnnotation); void addCollectionTypeRegistration(CollectionClassification classification, CollectionTypeRegistrationDescriptor descriptor); CollectionTypeRegistrationDescriptor findCollectionTypeRegistration(CollectionClassification classification); diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/AbstractPropertyHolder.java b/hibernate-core/src/main/java/org/hibernate/cfg/AbstractPropertyHolder.java index d3af3f930f..b2736d1ed8 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/AbstractPropertyHolder.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/AbstractPropertyHolder.java @@ -25,6 +25,8 @@ import jakarta.persistence.MappedSuperclass; import org.hibernate.AnnotationException; import org.hibernate.AssertionFailure; +import org.hibernate.annotations.ColumnTransformer; +import org.hibernate.annotations.ColumnTransformers; import org.hibernate.annotations.common.reflection.XAnnotatedElement; import org.hibernate.annotations.common.reflection.XClass; import org.hibernate.annotations.common.reflection.XProperty; @@ -47,6 +49,8 @@ public abstract class AbstractPropertyHolder implements PropertyHolder { protected AbstractPropertyHolder parent; private Map holderColumnOverride; private Map currentPropertyColumnOverride; + private Map holderColumnTransformerOverride; + private Map currentPropertyColumnTransformerOverride; private Map holderJoinColumnOverride; private Map currentPropertyJoinColumnOverride; private Map holderJoinTableOverride; @@ -170,6 +174,7 @@ public abstract class AbstractPropertyHolder implements PropertyHolder { protected void setCurrentProperty(XProperty property) { if ( property == null ) { this.currentPropertyColumnOverride = null; + this.currentPropertyColumnTransformerOverride = null; this.currentPropertyJoinColumnOverride = null; this.currentPropertyJoinTableOverride = null; this.currentPropertyForeignKeyOverride = null; @@ -180,6 +185,11 @@ public abstract class AbstractPropertyHolder implements PropertyHolder { this.currentPropertyColumnOverride = null; } + this.currentPropertyColumnTransformerOverride = buildColumnTransformerOverride( property, getPath() ); + if ( this.currentPropertyColumnTransformerOverride.size() == 0 ) { + this.currentPropertyColumnTransformerOverride = null; + } + this.currentPropertyJoinColumnOverride = buildJoinColumnOverride( property, getPath() ); if ( this.currentPropertyJoinColumnOverride.size() == 0 ) { this.currentPropertyJoinColumnOverride = null; @@ -247,6 +257,21 @@ public abstract class AbstractPropertyHolder implements PropertyHolder { return result; } + @Override + public ColumnTransformer getOverriddenColumnTransformer(String logicalColumnName) { + ColumnTransformer override = null; + if ( parent != null ) { + override = parent.getOverriddenColumnTransformer( logicalColumnName ); + } + if ( override == null && currentPropertyColumnTransformerOverride != null ) { + override = currentPropertyColumnTransformerOverride.get( logicalColumnName ); + } + if ( override == null && holderColumnTransformerOverride != null ) { + override = holderColumnTransformerOverride.get( logicalColumnName ); + } + return override; + } + /** * Get column overriding, property first, then parent, then holder * find the overridden rules from the exact property name. @@ -375,6 +400,7 @@ public abstract class AbstractPropertyHolder implements PropertyHolder { private void buildHierarchyColumnOverride(XClass element) { XClass current = element; Map columnOverride = new HashMap<>(); + Map columnTransformerOverride = new HashMap<>(); Map joinColumnOverride = new HashMap<>(); Map joinTableOverride = new HashMap<>(); Map foreignKeyOverride = new HashMap<>(); @@ -383,14 +409,17 @@ public abstract class AbstractPropertyHolder implements PropertyHolder { || current.isAnnotationPresent( Embeddable.class ) ) { //FIXME is embeddable override? Map currentOverride = buildColumnOverride( current, getPath() ); + Map currentTransformerOverride = buildColumnTransformerOverride( current, getPath() ); Map currentJoinOverride = buildJoinColumnOverride( current, getPath() ); Map currentJoinTableOverride = buildJoinTableOverride( current, getPath() ); Map currentForeignKeyOverride = buildForeignKeyOverride( current, getPath() ); currentOverride.putAll( columnOverride ); //subclasses have precedence over superclasses + currentTransformerOverride.putAll( columnTransformerOverride ); //subclasses have precedence over superclasses currentJoinOverride.putAll( joinColumnOverride ); //subclasses have precedence over superclasses currentJoinTableOverride.putAll( joinTableOverride ); //subclasses have precedence over superclasses currentForeignKeyOverride.putAll( foreignKeyOverride ); //subclasses have precedence over superclasses columnOverride = currentOverride; + columnTransformerOverride = currentTransformerOverride; joinColumnOverride = currentJoinOverride; joinTableOverride = currentJoinTableOverride; foreignKeyOverride = currentForeignKeyOverride; @@ -399,6 +428,7 @@ public abstract class AbstractPropertyHolder implements PropertyHolder { } holderColumnOverride = columnOverride.size() > 0 ? columnOverride : null; + holderColumnTransformerOverride = columnTransformerOverride.size() > 0 ? columnTransformerOverride : null; holderJoinColumnOverride = joinColumnOverride.size() > 0 ? joinColumnOverride : null; holderJoinTableOverride = joinTableOverride.size() > 0 ? joinTableOverride : null; holderForeignKeyOverride = foreignKeyOverride.size() > 0 ? foreignKeyOverride : null; @@ -448,6 +478,34 @@ public abstract class AbstractPropertyHolder implements PropertyHolder { return columnOverride; } + private static Map buildColumnTransformerOverride(XAnnotatedElement element, String path) { + Map columnOverride = new HashMap<>(); + if ( element != null ) { + ColumnTransformer singleOverride = element.getAnnotation( ColumnTransformer.class ); + ColumnTransformers multipleOverrides = element.getAnnotation( ColumnTransformers.class ); + ColumnTransformer[] overrides; + if ( singleOverride != null ) { + overrides = new ColumnTransformer[]{ singleOverride }; + } + else if ( multipleOverrides != null ) { + overrides = multipleOverrides.value(); + } + else { + overrides = null; + } + + if ( overrides != null ) { + for ( ColumnTransformer depAttr : overrides ) { + columnOverride.put( + depAttr.forColumn(), + depAttr + ); + } + } + } + return columnOverride; + } + private static Map buildJoinColumnOverride(XAnnotatedElement element, String path) { Map columnOverride = new HashMap<>(); if ( element != null ) { diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/AnnotatedColumn.java b/hibernate-core/src/main/java/org/hibernate/cfg/AnnotatedColumn.java index e3266fa6b0..b3fca1c5ad 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/AnnotatedColumn.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/AnnotatedColumn.java @@ -843,6 +843,9 @@ public class AnnotatedColumn { if ( inferredData != null ) { XProperty property = inferredData.getProperty(); if ( property != null ) { + if ( propertyHolder.isComponent() ) { + processExpression( propertyHolder.getOverriddenColumnTransformer( logicalColumnName ) ); + } processExpression( property.getAnnotation( ColumnTransformer.class ) ); ColumnTransformers annotations = property.getAnnotation( ColumnTransformers.class ); if (annotations != null) { 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 3a6b51e38c..b142c19952 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/AnnotationBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/AnnotationBinder.java @@ -11,6 +11,7 @@ import java.lang.annotation.Repeatable; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Member; import java.lang.reflect.Method; +import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -39,6 +40,9 @@ import org.hibernate.annotations.CollectionTypeRegistration; import org.hibernate.annotations.CollectionTypeRegistrations; import org.hibernate.annotations.Columns; import org.hibernate.annotations.Comment; +import org.hibernate.annotations.CompositeType; +import org.hibernate.annotations.CompositeTypeRegistration; +import org.hibernate.annotations.CompositeTypeRegistrations; import org.hibernate.annotations.DialectOverride; import org.hibernate.annotations.DialectOverride.OverridesAnnotation; import org.hibernate.annotations.DiscriminatorFormula; @@ -139,6 +143,9 @@ import org.hibernate.mapping.ToOne; import org.hibernate.mapping.UnionSubclass; import org.hibernate.metamodel.spi.EmbeddableInstantiator; import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.property.access.internal.PropertyAccessStrategyCompositeUserTypeImpl; +import org.hibernate.property.access.internal.PropertyAccessStrategyMixedImpl; +import org.hibernate.property.access.spi.PropertyAccessStrategy; import org.hibernate.resource.beans.spi.ManagedBean; import org.hibernate.resource.beans.spi.ManagedBeanRegistry; import org.hibernate.type.CustomType; @@ -147,6 +154,7 @@ import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.java.spi.JavaTypeRegistry; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.spi.TypeConfiguration; +import org.hibernate.usertype.CompositeUserType; import org.hibernate.usertype.UserType; import jakarta.persistence.AttributeConverter; @@ -392,6 +400,7 @@ public final class AnnotationBinder { handleTypeDescriptorRegistrations( pckg, context ); bindEmbeddableInstantiatorRegistrations( pckg, context ); + bindCompositeUserTypeRegistrations( pckg, context ); bindGenericGenerators( pckg, context ); bindQueries( pckg, context ); @@ -651,6 +660,7 @@ public final class AnnotationBinder { HashMap classGenerators = buildGenerators( clazzToProcess, context ); handleTypeDescriptorRegistrations( clazzToProcess, context ); bindEmbeddableInstantiatorRegistrations( clazzToProcess, context ); + bindCompositeUserTypeRegistrations( clazzToProcess, context ); // check properties final InheritanceState.ElementsToProcess elementsToProcess = inheritanceState.getElementsToProcess(); @@ -1111,6 +1121,32 @@ public final class AnnotationBinder { ); } + private static void bindCompositeUserTypeRegistrations(XAnnotatedElement annotatedElement, MetadataBuildingContext context) { + final CompositeTypeRegistration singleRegistration = + annotatedElement.getAnnotation( CompositeTypeRegistration.class ); + if ( singleRegistration != null ) { + handleCompositeUserTypeRegistration( context, singleRegistration ); + } + else { + final CompositeTypeRegistrations annotation = annotatedElement.getAnnotation( CompositeTypeRegistrations.class ); + if ( annotation != null ) { + final CompositeTypeRegistration[] registrations = annotation.value(); + for ( CompositeTypeRegistration registration : registrations ) { + handleCompositeUserTypeRegistration(context, registration); + } + } + } + } + + private static void handleCompositeUserTypeRegistration( + MetadataBuildingContext context, + CompositeTypeRegistration annotation) { + context.getMetadataCollector().registerCompositeUserType( + annotation.embeddableClass(), + annotation.userType() + ); + } + /** * Process all discriminator-related metadata per rules for "single table" inheritance */ @@ -1350,6 +1386,7 @@ public final class AnnotationBinder { true, false, null, + null, context, inheritanceStatePerClass ); @@ -2191,9 +2228,13 @@ public final class AnnotationBinder { || property.isAnnotationPresent( Embedded.class ) || property.isAnnotationPresent( EmbeddedId.class ) || returnedClass.isAnnotationPresent( Embeddable.class ); + final Class> compositeUserType = resolveCompositeUserType( + inferredData.getProperty(), + inferredData.getClassOrElement(), + context + ); - - if ( isComponent ) { + if ( isComponent || compositeUserType != null ) { String referencedEntityName = null; if ( isOverridden ) { PropertyData mapsIdProperty = getPropertyOverriddenByMapperOrMapsId( @@ -2214,6 +2255,7 @@ public final class AnnotationBinder { inheritanceStatePerClass, referencedEntityName, determineCustomInstantiator(property, returnedClass, context), + compositeUserType, isOverridden ? ( AnnotatedJoinColumn[] ) columns : null ); } @@ -2829,6 +2871,29 @@ public final class AnnotationBinder { return null; } + private static Class> resolveCompositeUserType( + XProperty property, + XClass returnedClass, + MetadataBuildingContext context) { + if ( property != null ) { + final CompositeType compositeType = property.getAnnotation( CompositeType.class ); + if ( compositeType != null ) { + return compositeType.value(); + } + } + + if ( returnedClass != null ) { + final Class embeddableClass = context.getBootstrapContext() + .getReflectionManager() + .toClass( returnedClass ); + if ( embeddableClass != null ) { + return context.getMetadataCollector().findRegisteredCompositeUserType( embeddableClass ); + } + } + + return null; + } + private static boolean isGlobalGeneratorNameGlobal(MetadataBuildingContext context) { return context.getBootstrapContext().getJpaCompliance().isGlobalGeneratorScopeEnabled(); } @@ -3051,6 +3116,7 @@ public final class AnnotationBinder { Map inheritanceStatePerClass, String referencedEntityName, //is a component who is overridden by a @MapsId Class customInstantiatorImpl, + Class> compositeUserTypeClass, AnnotatedJoinColumn[] columns) { Component comp; if ( referencedEntityName != null ) { @@ -3081,6 +3147,7 @@ public final class AnnotationBinder { isIdentifierMapper, false, customInstantiatorImpl, + compositeUserTypeClass, buildingContext, inheritanceStatePerClass ); @@ -3128,6 +3195,7 @@ public final class AnnotationBinder { boolean isIdentifierMapper, boolean inSecondPass, Class customInstantiatorImpl, + Class> compositeUserTypeClass, MetadataBuildingContext buildingContext, Map inheritanceStatePerClass) { return fillComponent( @@ -3141,6 +3209,7 @@ public final class AnnotationBinder { isIdentifierMapper, inSecondPass, customInstantiatorImpl, + compositeUserTypeClass, buildingContext, inheritanceStatePerClass ); @@ -3157,6 +3226,7 @@ public final class AnnotationBinder { boolean isIdentifierMapper, boolean inSecondPass, Class customInstantiatorImpl, + Class> compositeUserTypeClass, MetadataBuildingContext buildingContext, Map inheritanceStatePerClass) { /* @@ -3190,7 +3260,22 @@ public final class AnnotationBinder { final XClass xClassProcessed = inferredData.getPropertyClass(); List classElements = new ArrayList<>(); - XClass returnedClassOrElement = inferredData.getClassOrElement(); + + final CompositeUserType compositeUserType; + XClass returnedClassOrElement; + if ( compositeUserTypeClass == null ) { + compositeUserType = null; + returnedClassOrElement = inferredData.getClassOrElement(); + } + else { + compositeUserType = buildingContext.getBootstrapContext() + .getServiceRegistry() + .getService( ManagedBeanRegistry.class ) + .getBean( compositeUserTypeClass ) + .getBeanInstance(); + comp.setTypeName( compositeUserTypeClass.getName() ); + returnedClassOrElement = buildingContext.getBootstrapContext().getReflectionManager().toXClass( compositeUserType.embeddable() ); + } List baseClassElements = null; Map orderedBaseClassElements = new HashMap<>(); @@ -3295,6 +3380,7 @@ public final class AnnotationBinder { handleTypeDescriptorRegistrations( property, buildingContext ); bindEmbeddableInstantiatorRegistrations( property, buildingContext ); + bindCompositeUserTypeRegistrations( property, buildingContext ); } else { Map localGenerators = @@ -3310,6 +3396,24 @@ public final class AnnotationBinder { } } } + + if ( compositeUserType != null ) { + comp.sortProperties(); + final List sortedPropertyNames = new ArrayList<>( comp.getPropertySpan() ); + final List sortedPropertyTypes = new ArrayList<>( comp.getPropertySpan() ); + final PropertyAccessStrategy strategy = new PropertyAccessStrategyCompositeUserTypeImpl( compositeUserType, sortedPropertyNames, sortedPropertyTypes ); + for ( Property property : comp.getProperties() ) { + sortedPropertyNames.add( property.getName() ); + sortedPropertyTypes.add( + PropertyAccessStrategyMixedImpl.INSTANCE.buildPropertyAccess( + compositeUserType.embeddable(), + property.getName(), + false + ).getGetter().getReturnType() + ); + property.setPropertyAccessStrategy( strategy ); + } + } return comp; } @@ -3371,6 +3475,7 @@ public final class AnnotationBinder { false, false, null, + null, buildingContext, inheritanceStatePerClass ); diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/PropertyHolder.java b/hibernate-core/src/main/java/org/hibernate/cfg/PropertyHolder.java index d2e1fbdaf3..cd07750000 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/PropertyHolder.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/PropertyHolder.java @@ -11,6 +11,7 @@ import jakarta.persistence.ForeignKey; import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinTable; +import org.hibernate.annotations.ColumnTransformer; import org.hibernate.annotations.common.reflection.XClass; import org.hibernate.annotations.common.reflection.XProperty; import org.hibernate.boot.model.convert.spi.ConverterDescriptor; @@ -76,6 +77,8 @@ public interface PropertyHolder { return null; } + ColumnTransformer getOverriddenColumnTransformer(String logicalColumnName); + /** * return * - null if no join table is present, diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/annotations/CollectionBinder.java b/hibernate-core/src/main/java/org/hibernate/cfg/annotations/CollectionBinder.java index 12a8a78b31..9f7feb1058 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/annotations/CollectionBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/annotations/CollectionBinder.java @@ -26,6 +26,7 @@ import org.hibernate.annotations.CollectionIdJavaType; import org.hibernate.annotations.CollectionIdJdbcType; import org.hibernate.annotations.CollectionIdJdbcTypeCode; import org.hibernate.annotations.CollectionType; +import org.hibernate.annotations.CompositeType; import org.hibernate.annotations.Fetch; import org.hibernate.annotations.Filter; import org.hibernate.annotations.FilterJoinTable; @@ -43,6 +44,7 @@ import org.hibernate.annotations.ListIndexJdbcType; import org.hibernate.annotations.ListIndexJdbcTypeCode; import org.hibernate.annotations.Loader; import org.hibernate.annotations.ManyToAny; +import org.hibernate.annotations.MapKeyCustomCompositeType; import org.hibernate.annotations.OnDelete; import org.hibernate.annotations.OnDeleteAction; import org.hibernate.annotations.OptimisticLock; @@ -107,6 +109,7 @@ import org.hibernate.metamodel.spi.EmbeddableInstantiator; import org.hibernate.persister.collection.CollectionPersister; import org.hibernate.resource.beans.spi.ManagedBean; import org.hibernate.resource.beans.spi.ManagedBeanRegistry; +import org.hibernate.usertype.CompositeUserType; import org.hibernate.usertype.ParameterizedType; import org.hibernate.usertype.UserCollectionType; @@ -1731,7 +1734,12 @@ public abstract class CollectionBinder { } } - if ( AnnotatedClassType.EMBEDDABLE == classType ) { + final Class> compositeUserType = resolveCompositeUserType( + property, + elementClass, + buildingContext + ); + if ( AnnotatedClassType.EMBEDDABLE == classType || compositeUserType != null ) { holder.prepare(property); EntityBinder entityBinder = new EntityBinder(); @@ -1774,6 +1782,7 @@ public abstract class CollectionBinder { false, true, resolveCustomInstantiator(property, elementClass, buildingContext), + compositeUserType, buildingContext, inheritanceStatePerClass ); @@ -2104,6 +2113,27 @@ public abstract class CollectionBinder { return null; } + private static Class> resolveCompositeUserType( + XProperty property, + XClass returnedClass, + MetadataBuildingContext context) { + final CompositeType compositeType = property.getAnnotation( CompositeType.class ); + if ( compositeType != null ) { + return compositeType.value(); + } + + if ( returnedClass != null ) { + final Class embeddableClass = context.getBootstrapContext() + .getReflectionManager() + .toClass( returnedClass ); + if ( embeddableClass != null ) { + return context.getMetadataCollector().findRegisteredCompositeUserType( embeddableClass ); + } + } + + return null; + } + private String extractHqlOrderBy(jakarta.persistence.OrderBy jpaOrderBy) { if ( jpaOrderBy != null ) { return jpaOrderBy.value(); // Null not possible. In case of empty expression, apply default ordering. diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/annotations/MapBinder.java b/hibernate-core/src/main/java/org/hibernate/cfg/annotations/MapBinder.java index fdfdb41e57..f907721759 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/annotations/MapBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/annotations/MapBinder.java @@ -15,6 +15,7 @@ import org.hibernate.AnnotationException; import org.hibernate.AssertionFailure; import org.hibernate.FetchMode; import org.hibernate.MappingException; +import org.hibernate.annotations.MapKeyCustomCompositeType; import org.hibernate.annotations.common.reflection.XClass; import org.hibernate.annotations.common.reflection.XProperty; import org.hibernate.boot.spi.BootstrapContext; @@ -48,6 +49,7 @@ import org.hibernate.mapping.SimpleValue; import org.hibernate.mapping.Table; import org.hibernate.mapping.Value; import org.hibernate.resource.beans.spi.ManagedBean; +import org.hibernate.usertype.CompositeUserType; import org.hibernate.usertype.UserCollectionType; import jakarta.persistence.AttributeOverride; @@ -275,8 +277,12 @@ public class MapBinder extends CollectionBinder { else { throw new AssertionFailure( "Unable to guess collection property accessor name" ); } - - if ( AnnotatedClassType.EMBEDDABLE.equals( classType ) ) { + final Class> compositeUserType = resolveCompositeUserType( + property, + keyXClass, + buildingContext + ); + if ( AnnotatedClassType.EMBEDDABLE.equals( classType ) || compositeUserType != null ) { EntityBinder entityBinder = new EntityBinder(); PropertyData inferredData = isHibernateExtensionMapping() @@ -295,6 +301,7 @@ public class MapBinder extends CollectionBinder { false, true, null, + compositeUserType, buildingContext, inheritanceStatePerClass ); @@ -371,6 +378,27 @@ public class MapBinder extends CollectionBinder { } } + private static Class> resolveCompositeUserType( + XProperty property, + XClass returnedClass, + MetadataBuildingContext context) { + final MapKeyCustomCompositeType compositeType = property.getAnnotation( MapKeyCustomCompositeType.class ); + if ( compositeType != null ) { + return compositeType.value(); + } + + if ( returnedClass != null ) { + final Class embeddableClass = context.getBootstrapContext() + .getReflectionManager() + .toClass( returnedClass ); + if ( embeddableClass != null ) { + return context.getMetadataCollector().findRegisteredCompositeUserType( embeddableClass ); + } + } + + return null; + } + private jakarta.persistence.ForeignKey getMapKeyForeignKey(XProperty property) { final MapKeyJoinColumns mapKeyJoinColumns = property.getAnnotation( MapKeyJoinColumns.class ); if ( mapKeyJoinColumns != null ) { diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/annotations/PropertyBinder.java b/hibernate-core/src/main/java/org/hibernate/cfg/annotations/PropertyBinder.java index 6be4bc6d0f..01dc5e4eb8 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/annotations/PropertyBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/annotations/PropertyBinder.java @@ -40,6 +40,7 @@ import org.hibernate.mapping.SimpleValue; import org.hibernate.mapping.ToOne; import org.hibernate.mapping.Value; import org.hibernate.metamodel.spi.EmbeddableInstantiator; +import org.hibernate.property.access.spi.PropertyAccessStrategy; import org.hibernate.tuple.AnnotationValueGeneration; import org.hibernate.tuple.AttributeBinder; import org.hibernate.tuple.GenerationTiming; @@ -78,6 +79,7 @@ public class PropertyBinder { private EntityBinder entityBinder; private boolean isXToMany; private String referencedEntityName; + private PropertyAccessStrategy propertyAccessStrategy; public void setReferencedEntityName(String referencedEntityName) { this.referencedEntityName = referencedEntityName; @@ -151,6 +153,10 @@ public class PropertyBinder { this.buildingContext = buildingContext; } + public void setPropertyAccessStrategy(PropertyAccessStrategy propertyAccessStrategy) { + this.propertyAccessStrategy = propertyAccessStrategy; + } + public void setDeclaringClass(XClass declaringClass) { this.declaringClass = declaringClass; this.declaringClassSet = true; @@ -319,6 +325,7 @@ public class PropertyBinder { property.setInsertable( insertable ); property.setUpdateable( updatable ); + property.setPropertyAccessStrategy( propertyAccessStrategy ); inferOptimisticLocking(property); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java index b6b515319e..748c9f0078 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java @@ -3951,33 +3951,21 @@ public abstract class Dialect implements ConversionContext { Long length) { final Size size = new Size(); int jdbcTypeCode = jdbcType.getDefaultSqlTypeCode(); + // Set the explicit length to null if we encounter the JPA default 255 + if ( length != null && length == Size.DEFAULT_LENGTH ) { + length = null; + } - switch (jdbcTypeCode) { + switch ( jdbcTypeCode ) { case Types.BIT: - // Use the default length for Boolean if we encounter the JPA default 255 instead - if ( javaType.getJavaTypeClass() == Boolean.class && length != null && length == Size.DEFAULT_LENGTH ) { - length = null; - } - size.setLength( javaType.getDefaultSqlLength( Dialect.this, jdbcType ) ); - break; case Types.CHAR: case Types.NCHAR: - // Use the default length for char and UUID if we encounter the JPA default 255 instead - if ( length != null && length == Size.DEFAULT_LENGTH ) { - if ( javaType.getJavaTypeClass() == Character.class || javaType.getJavaTypeClass() == UUID.class ) { - length = null; - } - } - size.setLength( javaType.getDefaultSqlLength( Dialect.this, jdbcType ) ); - break; case Types.VARCHAR: case Types.NVARCHAR: case Types.BINARY: case Types.VARBINARY: - // Use the default length for UUID if we encounter the JPA default 255 instead - if ( javaType.getJavaTypeClass() == UUID.class && length != null && length == Size.DEFAULT_LENGTH ) { - length = null; - } + case Types.CLOB: + case Types.BLOB: size.setLength( javaType.getDefaultSqlLength( Dialect.this, jdbcType ) ); break; case Types.LONGVARCHAR: @@ -4009,13 +3997,6 @@ public abstract class Dialect implements ConversionContext { break; case Types.NUMERIC: case Types.DECIMAL: - size.setPrecision( javaType.getDefaultSqlPrecision( Dialect.this, jdbcType ) ); - size.setScale( javaType.getDefaultSqlScale( Dialect.this, jdbcType ) ); - break; - case Types.CLOB: - case Types.BLOB: - size.setLength( javaType.getDefaultSqlLength( Dialect.this, jdbcType ) ); - break; case SqlTypes.INTERVAL_SECOND: size.setPrecision( javaType.getDefaultSqlPrecision( Dialect.this, jdbcType ) ); size.setScale( javaType.getDefaultSqlScale( Dialect.this, jdbcType ) ); diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/Property.java b/hibernate-core/src/main/java/org/hibernate/mapping/Property.java index d30987f28a..a890af3faa 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/Property.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/Property.java @@ -45,6 +45,7 @@ public class Property implements Serializable, MetaAttributable { private boolean optimisticLocked = true; private ValueGeneration valueGenerationStrategy; private String propertyAccessorName; + private PropertyAccessStrategy propertyAccessStrategy; private boolean lazy; private String lazyGroup; private boolean optional; @@ -237,6 +238,14 @@ public class Property implements Serializable, MetaAttributable { propertyAccessorName = string; } + public PropertyAccessStrategy getPropertyAccessStrategy() { + return propertyAccessStrategy; + } + + public void setPropertyAccessStrategy(PropertyAccessStrategy propertyAccessStrategy) { + this.propertyAccessStrategy = propertyAccessStrategy; + } + /** * Approximate! */ @@ -355,6 +364,10 @@ public class Property implements Serializable, MetaAttributable { // todo : remove public PropertyAccessStrategy getPropertyAccessStrategy(Class clazz) throws MappingException { + final PropertyAccessStrategy propertyAccessStrategy = getPropertyAccessStrategy(); + if ( propertyAccessStrategy != null ) { + return propertyAccessStrategy; + } String accessName = getPropertyAccessorName(); if ( accessName == null ) { if ( clazz == null || java.util.Map.class.equals( clazz ) ) { @@ -438,6 +451,7 @@ public class Property implements Serializable, MetaAttributable { prop.setOptimisticLocked( isOptimisticLocked() ); prop.setValueGenerationStrategy( getValueGenerationStrategy() ); prop.setPropertyAccessorName( getPropertyAccessorName() ); + prop.setPropertyAccessStrategy( getPropertyAccessStrategy() ); prop.setLazy( isLazy() ); prop.setLazyGroup( getLazyGroup() ); prop.setOptional( isOptional() ); diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableCompositeUserTypeInstantiator.java b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableCompositeUserTypeInstantiator.java new file mode 100644 index 0000000000..bdf8cbf653 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EmbeddableCompositeUserTypeInstantiator.java @@ -0,0 +1,40 @@ +/* + * 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.util.function.Supplier; + +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.metamodel.spi.EmbeddableInstantiator; +import org.hibernate.usertype.CompositeUserType; + +/** + * @author Christian Beikov + */ +public class EmbeddableCompositeUserTypeInstantiator implements EmbeddableInstantiator { + + private final CompositeUserType userType; + + public EmbeddableCompositeUserTypeInstantiator(CompositeUserType userType) { + this.userType = userType; + } + + @Override + public Object instantiate(Supplier valuesAccess, SessionFactoryImplementor sessionFactory) { + return userType.instantiate( valuesAccess, sessionFactory ); + } + + @Override + public boolean isInstance(Object object, SessionFactoryImplementor sessionFactory) { + return userType.returnedClass().isInstance( object ); + } + + @Override + public boolean isSameClass(Object object, SessionFactoryImplementor sessionFactory) { + return object.getClass().equals( userType.returnedClass() ); + } +} 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 ae5381f176..f59c3da208 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 @@ -31,6 +31,10 @@ import org.hibernate.property.access.internal.PropertyAccessStrategyIndexBackRef import org.hibernate.property.access.spi.BuiltInPropertyAccessStrategies; import org.hibernate.property.access.spi.PropertyAccess; import org.hibernate.property.access.spi.PropertyAccessStrategy; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.java.spi.JavaTypeRegistry; +import org.hibernate.type.internal.CompositeUserTypeJavaTypeWrapper; +import org.hibernate.usertype.CompositeUserType; /** * @author Steve Ebersole @@ -45,12 +49,11 @@ public class EmbeddableRepresentationStrategyPojo extends AbstractEmbeddableRepr Component bootDescriptor, Supplier runtimeDescriptorAccess, EmbeddableInstantiator customInstantiator, + CompositeUserType compositeUserType, RuntimeModelCreationContext creationContext) { super( bootDescriptor, - creationContext.getTypeConfiguration() - .getJavaTypeRegistry() - .resolveDescriptor( bootDescriptor.getComponentClass() ), + resolveEmbeddableJavaType( bootDescriptor, compositeUserType, creationContext ), creationContext ); @@ -75,6 +78,20 @@ public class EmbeddableRepresentationStrategyPojo extends AbstractEmbeddableRepr : determineInstantiator( bootDescriptor, runtimeDescriptorAccess, creationContext ); } + private static JavaType resolveEmbeddableJavaType( + Component bootDescriptor, + CompositeUserType compositeUserType, + RuntimeModelCreationContext creationContext) { + final JavaTypeRegistry javaTypeRegistry = creationContext.getTypeConfiguration().getJavaTypeRegistry(); + if ( compositeUserType == null ) { + return javaTypeRegistry.resolveDescriptor( bootDescriptor.getComponentClass() ); + } + return javaTypeRegistry.resolveDescriptor( + compositeUserType.returnedClass(), + () -> new CompositeUserTypeJavaTypeWrapper<>( compositeUserType ) + ); + } + private EmbeddableInstantiator determineInstantiator( Component bootDescriptor, Supplier runtimeDescriptorAccess, diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EntityRepresentationStrategyPojoStandard.java b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EntityRepresentationStrategyPojoStandard.java index 14a7b398bc..25fad70ac5 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EntityRepresentationStrategyPojoStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/EntityRepresentationStrategyPojoStandard.java @@ -117,6 +117,7 @@ public class EntityRepresentationStrategyPojoStandard implements EntityRepresent .getMappingModelPart().getEmbeddableTypeDescriptor(), // we currently do not support custom instantiators for identifiers null, + null, creationContext ); } @@ -127,6 +128,7 @@ public class EntityRepresentationStrategyPojoStandard implements EntityRepresent .getMappingModelPart().getEmbeddableTypeDescriptor(), // we currently do not support custom instantiators for identifiers null, + null, creationContext ); } 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 819927cd70..761e97cdc2 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 @@ -20,6 +20,7 @@ import org.hibernate.metamodel.spi.ManagedTypeRepresentationResolver; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.resource.beans.spi.ManagedBeanRegistry; +import org.hibernate.usertype.CompositeUserType; /** * @author Steve Ebersole @@ -76,6 +77,21 @@ public class ManagedTypeRepresentationResolverStandard implements ManagedTypeRep } } + final CompositeUserType compositeUserType; + if ( bootDescriptor.getTypeName() != null ) { + compositeUserType = (CompositeUserType) creationContext.getBootstrapContext() + .getServiceRegistry() + .getService( ManagedBeanRegistry.class ) + .getBean( + creationContext.getBootstrapContext() + .getClassLoaderAccess() + .classForName( bootDescriptor.getTypeName() ) + ) + .getBeanInstance(); + } + else { + compositeUserType = null; + } final EmbeddableInstantiator customInstantiator; if ( bootDescriptor.getCustomInstantiator() != null ) { final Class customInstantiatorImpl = bootDescriptor.getCustomInstantiator(); @@ -85,6 +101,9 @@ public class ManagedTypeRepresentationResolverStandard implements ManagedTypeRep .getBean( customInstantiatorImpl ) .getBeanInstance(); } + else if ( compositeUserType != null ) { + customInstantiator = new EmbeddableCompositeUserTypeInstantiator( compositeUserType ); + } else { customInstantiator = null; } @@ -108,6 +127,7 @@ public class ManagedTypeRepresentationResolverStandard implements ManagedTypeRep bootDescriptor, runtimeDescriptorAccess, customInstantiator, + compositeUserType, creationContext ); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/BasicValuedCollectionPart.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/BasicValuedCollectionPart.java index 67d2715d26..c927fcf0b8 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/BasicValuedCollectionPart.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/BasicValuedCollectionPart.java @@ -213,7 +213,7 @@ public class BasicValuedCollectionPart @Override public String getFetchableName() { - return nature == Nature.ELEMENT ? "{value}" : "{key}"; + return nature.getName(); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/property/access/internal/PropertyAccessCompositeUserTypeImpl.java b/hibernate-core/src/main/java/org/hibernate/property/access/internal/PropertyAccessCompositeUserTypeImpl.java new file mode 100644 index 0000000000..7d84fab191 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/property/access/internal/PropertyAccessCompositeUserTypeImpl.java @@ -0,0 +1,87 @@ +/* + * 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.property.access.internal; + +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.Map; + +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.internal.util.ReflectHelper; +import org.hibernate.property.access.spi.Getter; +import org.hibernate.property.access.spi.PropertyAccess; +import org.hibernate.property.access.spi.PropertyAccessStrategy; +import org.hibernate.property.access.spi.Setter; + +/** + * PropertyAccessor for accessing the wrapped property via get/set pair, which may be nonpublic. + * + * @author Steve Ebersole + * + * @see PropertyAccessStrategyBasicImpl + */ +public class PropertyAccessCompositeUserTypeImpl implements PropertyAccess, Getter { + + private final PropertyAccessStrategyCompositeUserTypeImpl strategy; + private final int propertyIndex; + + public PropertyAccessCompositeUserTypeImpl(PropertyAccessStrategyCompositeUserTypeImpl strategy, String property) { + this.strategy = strategy; + this.propertyIndex = strategy.sortedPropertyNames.indexOf( property ); + } + + @Override + public PropertyAccessStrategy getPropertyAccessStrategy() { + return strategy; + } + + @Override + public Getter getGetter() { + return this; + } + + @Override + public Setter getSetter() { + return null; + } + + @Override + public Object get(Object owner) { + return strategy.compositeUserType.getPropertyValue( owner, propertyIndex ); + } + + @Override + public Object getForInsert(Object owner, Map mergeMap, SharedSessionContractImplementor session) { + return get( owner ); + } + + @Override + public Class getReturnTypeClass() { + return ReflectHelper.getClass( strategy.sortedPropertyTypes.get(propertyIndex) ); + } + + @Override + public Type getReturnType() { + return strategy.sortedPropertyTypes.get(propertyIndex); + } + + @Override + public Member getMember() { + return null; + } + + @Override + public String getMethodName() { + return null; + } + + @Override + public Method getMethod() { + return null; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/property/access/internal/PropertyAccessStrategyCompositeUserTypeImpl.java b/hibernate-core/src/main/java/org/hibernate/property/access/internal/PropertyAccessStrategyCompositeUserTypeImpl.java new file mode 100644 index 0000000000..40d5ae20a2 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/property/access/internal/PropertyAccessStrategyCompositeUserTypeImpl.java @@ -0,0 +1,41 @@ +/* + * 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.property.access.internal; + +import java.lang.reflect.Type; +import java.util.List; + +import org.hibernate.property.access.spi.PropertyAccess; +import org.hibernate.property.access.spi.PropertyAccessStrategy; +import org.hibernate.usertype.CompositeUserType; + +/** + * Defines a strategy for accessing property values via a CompositeUserType. + * + * @author Christian Beikov + */ +public class PropertyAccessStrategyCompositeUserTypeImpl implements PropertyAccessStrategy { + + final CompositeUserType compositeUserType; + final List sortedPropertyNames; + final List sortedPropertyTypes; + + public PropertyAccessStrategyCompositeUserTypeImpl( + CompositeUserType compositeUserType, + List sortedPropertyNames, + List sortedPropertyTypes) { + //noinspection unchecked + this.compositeUserType = (CompositeUserType) compositeUserType; + this.sortedPropertyNames = sortedPropertyNames; + this.sortedPropertyTypes = sortedPropertyTypes; + } + + @Override + public PropertyAccess buildPropertyAccess(Class containerJavaType, final String propertyName, boolean setterRequired) { + return new PropertyAccessCompositeUserTypeImpl( this, propertyName ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/internal/FetchMementoHbmStandard.java b/hibernate-core/src/main/java/org/hibernate/query/internal/FetchMementoHbmStandard.java index 8cf1626da2..ca60e4a36a 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/internal/FetchMementoHbmStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/query/internal/FetchMementoHbmStandard.java @@ -12,9 +12,12 @@ import java.util.Map; import java.util.function.Consumer; import org.hibernate.LockMode; +import org.hibernate.metamodel.mapping.CollectionPart; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.PluralAttributeMapping; import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping; +import org.hibernate.query.results.complete.CompleteFetchBuilderEntityValuedModelPart; +import org.hibernate.query.results.dynamic.DynamicFetchBuilder; import org.hibernate.query.results.dynamic.DynamicResultBuilderEntityStandard; import org.hibernate.query.spi.NavigablePath; import org.hibernate.query.named.FetchMemento; @@ -27,6 +30,9 @@ import org.hibernate.sql.results.graph.FetchableContainer; * @author Steve Ebersole */ public class FetchMementoHbmStandard implements FetchMemento, FetchMemento.Parent { + + private static final String ELEMENT_PREFIX = "element."; + public interface FetchParentMemento { NavigablePath getNavigablePath(); FetchableContainer getFetchableContainer(); @@ -84,6 +90,28 @@ public class FetchMementoHbmStandard implements FetchMemento, FetchMemento.Paren tableAlias, navigablePath ); + FetchBuilder element = fetchBuilderMap.get( "element" ); + if ( element != null ) { + if ( element instanceof DynamicFetchBuilder ) { + resultBuilder.addIdColumnAliases( + ( (DynamicFetchBuilder) element ).getColumnAliases().toArray( new String[0] ) + ); + } + else { + resultBuilder.addIdColumnAliases( + ( (CompleteFetchBuilderEntityValuedModelPart) element ).getColumnAliases().toArray( new String[0] ) + ); + } + } + FetchBuilder index = fetchBuilderMap.get( "index" ); + if ( index != null ) { + resultBuilder.addFetchBuilder( CollectionPart.Nature.INDEX.getName(), index ); + } + for ( Map.Entry entry : fetchBuilderMap.entrySet() ) { + if ( entry.getKey().startsWith( ELEMENT_PREFIX ) ) { + resultBuilder.addFetchBuilder( entry.getKey().substring( ELEMENT_PREFIX.length() ), entry.getValue() ); + } + } } else { resultBuilder = new DynamicResultBuilderEntityStandard( @@ -91,6 +119,7 @@ public class FetchMementoHbmStandard implements FetchMemento, FetchMemento.Paren tableAlias, navigablePath ); + fetchBuilderMap.forEach( resultBuilder::addFetchBuilder ); } return new DynamicFetchBuilderLegacy( tableAlias, diff --git a/hibernate-core/src/main/java/org/hibernate/query/results/complete/CompleteFetchBuilderEntityValuedModelPart.java b/hibernate-core/src/main/java/org/hibernate/query/results/complete/CompleteFetchBuilderEntityValuedModelPart.java index 793a96c04b..31f627d412 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/results/complete/CompleteFetchBuilderEntityValuedModelPart.java +++ b/hibernate-core/src/main/java/org/hibernate/query/results/complete/CompleteFetchBuilderEntityValuedModelPart.java @@ -66,6 +66,10 @@ public class CompleteFetchBuilderEntityValuedModelPart return modelPart; } + public List getColumnAliases() { + return columnAliases; + } + @Override public Fetch buildFetch( FetchParent parent, diff --git a/hibernate-core/src/main/java/org/hibernate/query/results/dynamic/AbstractFetchBuilderContainer.java b/hibernate-core/src/main/java/org/hibernate/query/results/dynamic/AbstractFetchBuilderContainer.java index ae8b51360b..9c824e690d 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/results/dynamic/AbstractFetchBuilderContainer.java +++ b/hibernate-core/src/main/java/org/hibernate/query/results/dynamic/AbstractFetchBuilderContainer.java @@ -19,16 +19,16 @@ import org.hibernate.query.results.FetchBuilder; */ public abstract class AbstractFetchBuilderContainer> implements DynamicFetchBuilderContainer { - private Map fetchBuilderMap; + private Map fetchBuilderMap; protected AbstractFetchBuilderContainer() { } protected AbstractFetchBuilderContainer(AbstractFetchBuilderContainer original) { if ( original.fetchBuilderMap != null ) { - final Map fetchBuilderMap = new HashMap<>( original.fetchBuilderMap.size() ); - for ( Map.Entry entry : original.fetchBuilderMap.entrySet() ) { - final DynamicFetchBuilder fetchBuilder; + final Map fetchBuilderMap = new HashMap<>( original.fetchBuilderMap.size() ); + for ( Map.Entry entry : original.fetchBuilderMap.entrySet() ) { + final FetchBuilder fetchBuilder; if ( entry.getValue() instanceof DynamicFetchBuilderStandard ) { fetchBuilder = ( (DynamicFetchBuilderStandard) entry.getValue() ).cacheKeyInstance( this ); } @@ -44,7 +44,7 @@ public abstract class AbstractFetchBuilderContainer(); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/results/dynamic/DynamicFetchBuilderContainer.java b/hibernate-core/src/main/java/org/hibernate/query/results/dynamic/DynamicFetchBuilderContainer.java index 240d68587f..3d5bbb7e95 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/results/dynamic/DynamicFetchBuilderContainer.java +++ b/hibernate-core/src/main/java/org/hibernate/query/results/dynamic/DynamicFetchBuilderContainer.java @@ -6,6 +6,8 @@ */ package org.hibernate.query.results.dynamic; +import org.hibernate.query.results.FetchBuilder; + /** * @author Steve Ebersole */ @@ -13,7 +15,7 @@ public interface DynamicFetchBuilderContainer { /** * Locate an explicit fetch definition for the named fetchable */ - DynamicFetchBuilder findFetchBuilder(String fetchableName); + FetchBuilder findFetchBuilder(String fetchableName); /** * Add a property mapped to a single column. @@ -29,4 +31,6 @@ public interface DynamicFetchBuilderContainer { * Add a property whose columns can later be defined using {@link DynamicFetchBuilder#addColumnAlias} */ DynamicFetchBuilder addProperty(String propertyName); + + void addFetchBuilder(String propertyName, FetchBuilder fetchBuilder); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/results/dynamic/DynamicFetchBuilderLegacy.java b/hibernate-core/src/main/java/org/hibernate/query/results/dynamic/DynamicFetchBuilderLegacy.java index 3b09b7138d..c816d16460 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/results/dynamic/DynamicFetchBuilderLegacy.java +++ b/hibernate-core/src/main/java/org/hibernate/query/results/dynamic/DynamicFetchBuilderLegacy.java @@ -16,6 +16,7 @@ import java.util.function.BiFunction; import org.hibernate.LockMode; import org.hibernate.engine.FetchTiming; import org.hibernate.metamodel.mapping.AttributeMapping; +import org.hibernate.metamodel.mapping.CollectionPart; import org.hibernate.metamodel.mapping.ForeignKeyDescriptor; import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.metamodel.mapping.PluralAttributeMapping; @@ -44,7 +45,10 @@ import static org.hibernate.sql.ast.spi.SqlExpressionResolver.createColumnRefere * @author Steve Ebersole * @author Christian Beikov */ -public class DynamicFetchBuilderLegacy implements DynamicFetchBuilder, NativeQuery.FetchReturn { +public class DynamicFetchBuilderLegacy implements DynamicFetchBuilder, NativeQuery.FetchReturn, DynamicFetchBuilderContainer { + + private static final String ELEMENT_PREFIX = CollectionPart.Nature.ELEMENT.getName() + "."; + private static final String INDEX_PREFIX = CollectionPart.Nature.INDEX.getName() + "."; private final String tableAlias; @@ -112,7 +116,7 @@ public class DynamicFetchBuilderLegacy implements DynamicFetchBuilder, NativeQue tableAlias, ownerTableAlias, fetchableName, - List.copyOf( columnNames ), + columnNames == null ? null : List.copyOf( columnNames ), fetchBuilderMap, resultBuilderEntity == null ? null : resultBuilderEntity.cacheKeyInstance() ); @@ -193,14 +197,37 @@ public class DynamicFetchBuilderLegacy implements DynamicFetchBuilder, NativeQue ); } } - return parent.generateFetchableFetch( - attributeMapping, - parent.resolveNavigablePath( attributeMapping ), - FetchTiming.IMMEDIATE, - true, - null, - domainResultCreationState - ); + try { + final NavigablePath currentRelativePath = creationState.getCurrentRelativePath(); + final String prefix; + if ( currentRelativePath == null ) { + prefix = ""; + } + else { + prefix = currentRelativePath.getFullPath() + .replace( ELEMENT_PREFIX, "" ) + .replace( INDEX_PREFIX, "" ) + "."; + } + creationState.pushExplicitFetchMementoResolver( + relativePath -> { + if ( relativePath.startsWith( prefix ) ) { + return findFetchBuilder( relativePath.substring( prefix.length() ) ); + } + return null; + } + ); + return parent.generateFetchableFetch( + attributeMapping, + parent.resolveNavigablePath( attributeMapping ), + FetchTiming.IMMEDIATE, + true, + null, + domainResultCreationState + ); + } + finally { + creationState.popExplicitFetchMementoResolver(); + } } private void resolveSqlSelection( @@ -242,18 +269,37 @@ public class DynamicFetchBuilderLegacy implements DynamicFetchBuilder, NativeQue } @Override - public NativeQuery.FetchReturn addProperty(String propertyName, String columnAlias) { + public DynamicFetchBuilderLegacy addProperty(String propertyName, String columnAlias) { addProperty( propertyName ).addColumnAlias( columnAlias ); return this; } @Override - public NativeQuery.ReturnProperty addProperty(String propertyName) { + public DynamicFetchBuilder addProperty(String propertyName) { DynamicFetchBuilderStandard fetchBuilder = new DynamicFetchBuilderStandard( propertyName ); fetchBuilderMap.put( propertyName, fetchBuilder ); return fetchBuilder; } + @Override + public FetchBuilder findFetchBuilder(String fetchableName) { + return fetchBuilderMap.get( fetchableName ); + } + + @Override + public DynamicFetchBuilderContainer addProperty(String propertyName, String... columnAliases) { + final DynamicFetchBuilder fetchBuilder = addProperty( propertyName ); + for ( String columnAlias : columnAliases ) { + fetchBuilder.addColumnAlias( columnAlias ); + } + return this; + } + + @Override + public void addFetchBuilder(String propertyName, FetchBuilder fetchBuilder) { + fetchBuilderMap.put( propertyName, fetchBuilder ); + } + @Override public void visitFetchBuilders(BiConsumer consumer) { fetchBuilderMap.forEach( consumer ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/results/dynamic/DynamicFetchBuilderStandard.java b/hibernate-core/src/main/java/org/hibernate/query/results/dynamic/DynamicFetchBuilderStandard.java index 11b83c559f..57cf3afe98 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/results/dynamic/DynamicFetchBuilderStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/query/results/dynamic/DynamicFetchBuilderStandard.java @@ -127,7 +127,8 @@ public class DynamicFetchBuilderStandard } else if ( attributeMapping instanceof ToOneAttributeMapping ) { final ToOneAttributeMapping toOneAttributeMapping = (ToOneAttributeMapping) attributeMapping; - toOneAttributeMapping.getForeignKeyDescriptor().visitKeySelectables( selectableConsumer ); + toOneAttributeMapping.getForeignKeyDescriptor().getPart( toOneAttributeMapping.getSideNature() ) + .forEachSelectable( selectableConsumer ); return parent.generateFetchableFetch( attributeMapping, fetchPath, diff --git a/hibernate-core/src/main/java/org/hibernate/query/results/dynamic/DynamicResultBuilderEntityStandard.java b/hibernate-core/src/main/java/org/hibernate/query/results/dynamic/DynamicResultBuilderEntityStandard.java index cefe8779b9..b273a54884 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/results/dynamic/DynamicResultBuilderEntityStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/query/results/dynamic/DynamicResultBuilderEntityStandard.java @@ -21,6 +21,7 @@ import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.metamodel.mapping.internal.SingleAttributeIdentifierMapping; import org.hibernate.query.NativeQuery; +import org.hibernate.query.results.FetchBuilder; import org.hibernate.query.spi.NavigablePath; import org.hibernate.query.results.DomainResultCreationStateImpl; import org.hibernate.query.results.ResultSetMappingSqlSelection; @@ -49,6 +50,7 @@ public class DynamicResultBuilderEntityStandard implements DynamicResultBuilderEntity, NativeQuery.RootReturn { private static final String ELEMENT_PREFIX = CollectionPart.Nature.ELEMENT.getName() + "."; + private static final String INDEX_PREFIX = CollectionPart.Nature.INDEX.getName() + "."; private final NavigablePath navigablePath; @@ -192,12 +194,12 @@ public class DynamicResultBuilderEntityStandard ); final TableReference tableReference = tableGroup.getPrimaryTableReference(); final List idColumnAliases; - final DynamicFetchBuilder idFetchBuilder; + final FetchBuilder idFetchBuilder; if ( this.idColumnNames != null ) { idColumnAliases = this.idColumnNames; } else if ( ( idFetchBuilder = findIdFetchBuilder() ) != null ) { - idColumnAliases = idFetchBuilder.getColumnAliases(); + idColumnAliases = ( (DynamicFetchBuilder) idFetchBuilder ).getColumnAliases(); } else { idColumnAliases = null; @@ -237,7 +239,9 @@ public class DynamicResultBuilderEntityStandard prefix = ""; } else { - prefix = currentRelativePath.getFullPath() + "."; + prefix = currentRelativePath.getFullPath() + .replace( ELEMENT_PREFIX, "" ) + .replace( INDEX_PREFIX, "" ) + "."; } creationState.pushExplicitFetchMementoResolver( relativePath -> { @@ -261,7 +265,7 @@ public class DynamicResultBuilderEntityStandard } } - private DynamicFetchBuilder findIdFetchBuilder() { + private FetchBuilder findIdFetchBuilder() { final EntityIdentifierMapping identifierMapping = entityMapping.getIdentifierMapping(); if ( identifierMapping instanceof SingleAttributeIdentifierMapping ) { return findFetchBuilder( ( (SingleAttributeIdentifierMapping) identifierMapping ).getAttributeName() ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/ResultSetMappingProcessor.java b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/ResultSetMappingProcessor.java index eaa02f2587..97f28bb55c 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/ResultSetMappingProcessor.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/ResultSetMappingProcessor.java @@ -24,6 +24,7 @@ import org.hibernate.internal.CoreLogging; import org.hibernate.internal.CoreMessageLogger; import org.hibernate.internal.util.collections.ArrayHelper; import org.hibernate.loader.internal.AliasConstantsHelper; +import org.hibernate.metamodel.mapping.CollectionPart; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.persister.collection.CollectionPersister; import org.hibernate.persister.collection.QueryableCollection; @@ -33,6 +34,7 @@ import org.hibernate.persister.entity.Joinable; import org.hibernate.persister.entity.Loadable; import org.hibernate.persister.entity.SQLLoadable; import org.hibernate.query.NativeQuery; +import org.hibernate.query.results.dynamic.DynamicFetchBuilderContainer; import org.hibernate.query.spi.NavigablePath; import org.hibernate.query.results.FetchBuilder; import org.hibernate.query.results.ResultSetMapping; @@ -41,6 +43,7 @@ import org.hibernate.query.results.complete.CompleteResultBuilderCollectionStand import org.hibernate.query.results.dynamic.DynamicFetchBuilderLegacy; import org.hibernate.query.results.dynamic.DynamicResultBuilderEntityStandard; import org.hibernate.type.CollectionType; +import org.hibernate.type.ComponentType; import org.hibernate.type.EntityType; import org.hibernate.type.Type; @@ -254,7 +257,7 @@ public class ResultSetMappingProcessor implements SQLQueryParser.ParserContext { columnNames = Arrays.asList( keyColumnAliases ); if ( collectionPersister.hasIndex() ) { resultBuilderEntity.addProperty( - "{key}", // That's what BasicValuedCollectionPart returns.. + CollectionPart.Nature.INDEX.getName(), collectionPersister.getIndexColumnAliases( collectionSuffix ) ); } @@ -326,30 +329,79 @@ public class ResultSetMappingProcessor implements SQLQueryParser.ParserContext { for ( String propertyName : loadable.getPropertyNames() ) { final String[] columnAliases = loadable.getSubclassPropertyColumnAliases( propertyName, suffix ); - if ( columnAliases.length == 0 ) { - final Type propertyType = loadable.getPropertyType( propertyName ); - if ( propertyType instanceof CollectionType ) { - final CollectionType collectionType = (CollectionType) propertyType; - final String[] keyColumnAliases; - if ( collectionType.useLHSPrimaryKey() ) { - keyColumnAliases = identifierAliases; - } - else { - keyColumnAliases = loadable.getSubclassPropertyColumnAliases( - collectionType.getLHSPropertyName(), - suffix - ); - } - resultBuilderEntity.addProperty( propertyName, keyColumnAliases ); - } - } - else { - resultBuilderEntity.addProperty( propertyName, columnAliases ); - } + final Type propertyType = loadable.getPropertyType( propertyName ); + addFetchBuilder( + suffix, + loadable, + resultBuilderEntity, + tableAlias, + identifierAliases, + propertyName, + columnAliases, + propertyType + ); } return resultBuilderEntity; } + private void addFetchBuilder( + String suffix, + SQLLoadable loadable, + DynamicFetchBuilderContainer resultBuilderEntity, + String tableAlias, + String[] identifierAliases, + String propertyName, + String[] columnAliases, + Type propertyType) { + if ( propertyType instanceof CollectionType ) { + final CollectionType collectionType = (CollectionType) propertyType; + final String[] keyColumnAliases; + if ( collectionType.useLHSPrimaryKey() ) { + keyColumnAliases = identifierAliases; + } + else { + keyColumnAliases = loadable.getSubclassPropertyColumnAliases( + collectionType.getLHSPropertyName(), + suffix + ); + } + resultBuilderEntity.addProperty( propertyName, keyColumnAliases ); + } + else if ( propertyType instanceof ComponentType ) { + final Map fetchBuilderMap = new HashMap<>(); + final DynamicFetchBuilderLegacy fetchBuilder = new DynamicFetchBuilderLegacy( + "", + tableAlias, + propertyName, + null, + fetchBuilderMap + ); + final ComponentType componentType = (ComponentType) propertyType; + final String[] propertyNames = componentType.getPropertyNames(); + final Type[] propertyTypes = componentType.getSubtypes(); + int aliasIndex = 0; + for ( int i = 0; i < propertyNames.length; i++ ) { + final int columnSpan = propertyTypes[i].getColumnSpan( loadable.getFactory() ); + addFetchBuilder( + suffix, + loadable, + fetchBuilder, + tableAlias, + identifierAliases, + propertyNames[i], + ArrayHelper.slice( columnAliases, aliasIndex, columnSpan ), + propertyTypes[i] + ); + aliasIndex += columnSpan; + } + + resultBuilderEntity.addFetchBuilder( propertyName, fetchBuilder ); + } + else if ( columnAliases.length != 0 ) { + resultBuilderEntity.addProperty( propertyName, columnAliases ); + } + } + private CompleteResultBuilderCollectionStandard createSuffixedResultBuilder( NativeQuery.CollectionReturn collectionReturn, String suffix, diff --git a/hibernate-core/src/main/java/org/hibernate/type/ComponentType.java b/hibernate-core/src/main/java/org/hibernate/type/ComponentType.java index 7c923d8d72..8a8cfd5fa9 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/ComponentType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/ComponentType.java @@ -37,11 +37,13 @@ import org.hibernate.metamodel.mapping.EmbeddableValuedModelPart; import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess; import org.hibernate.property.access.spi.PropertyAccess; import org.hibernate.query.sqm.SqmExpressible; +import org.hibernate.resource.beans.spi.ManagedBeanRegistry; import org.hibernate.tuple.PropertyFactory; import org.hibernate.tuple.StandardProperty; import org.hibernate.tuple.ValueGeneration; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.spi.CompositeTypeImplementor; +import org.hibernate.usertype.CompositeUserType; /** * Handles "component" mappings @@ -63,6 +65,7 @@ public class ComponentType extends AbstractType implements CompositeTypeImplemen private final boolean isKey; private boolean hasNotNullProperty; private final boolean createEmptyCompositesEnabled; + private final CompositeUserType compositeUserType; private EmbeddableValuedModelPart mappingModelPart; @@ -102,6 +105,21 @@ public class ComponentType extends AbstractType implements CompositeTypeImplemen buildingContext.getBootstrapContext().getServiceRegistry().getService( ConfigurationService.class ).getSettings(), false ); + if ( component.getTypeName() != null ) { + //noinspection unchecked + this.compositeUserType = (CompositeUserType) buildingContext.getBootstrapContext() + .getServiceRegistry() + .getService( ManagedBeanRegistry.class ) + .getBean( + buildingContext.getBootstrapContext() + .getClassLoaderAccess() + .classForName( component.getTypeName() ) + ) + .getBeanInstance(); + } + else { + this.compositeUserType = null; + } } public boolean isKey() { @@ -162,6 +180,9 @@ public class ComponentType extends AbstractType implements CompositeTypeImplemen if ( x == y ) { return true; } + if ( compositeUserType != null ) { + return compositeUserType.equals( x, y ); + } // null value and empty component are considered equivalent for ( int i = 0; i < propertySpan; i++ ) { if ( !propertyTypes[i].isEqual( getPropertyValue( x, i ), getPropertyValue( y, i ) ) ) { @@ -177,6 +198,9 @@ public class ComponentType extends AbstractType implements CompositeTypeImplemen if ( x == y ) { return true; } + if ( compositeUserType != null ) { + return compositeUserType.equals( x, y ); + } // null value and empty component are considered equivalent for ( int i = 0; i < propertySpan; i++ ) { if ( !propertyTypes[i].isEqual( getPropertyValue( x, i ), getPropertyValue( y, i ), factory ) ) { @@ -206,6 +230,9 @@ public class ComponentType extends AbstractType implements CompositeTypeImplemen @Override public int getHashCode(final Object x) { + if ( compositeUserType != null ) { + return compositeUserType.hashCode( x ); + } int result = 17; for ( int i = 0; i < propertySpan; i++ ) { Object y = getPropertyValue( x, i ); @@ -219,6 +246,9 @@ public class ComponentType extends AbstractType implements CompositeTypeImplemen @Override public int getHashCode(final Object x, final SessionFactoryImplementor factory) { + if ( compositeUserType != null ) { + return compositeUserType.hashCode( x ); + } int result = 17; for ( int i = 0; i < propertySpan; i++ ) { Object y = getPropertyValue( x, i ); @@ -456,6 +486,9 @@ public class ComponentType extends AbstractType implements CompositeTypeImplemen return null; } + if ( compositeUserType != null ) { + return compositeUserType.deepCopy( component ); + } final Object[] values = getPropertyValues( component ); for ( int i = 0; i < propertySpan; i++ ) { values[i] = propertyTypes[i].deepCopy( values[i], factory ); @@ -487,6 +520,9 @@ public class ComponentType extends AbstractType implements CompositeTypeImplemen if ( original == null ) { return null; } + if ( compositeUserType != null ) { + return compositeUserType.replace( original, target, owner ); + } //if ( original == target ) return target; final Object[] originalValues = getPropertyValues( original ); @@ -532,6 +568,9 @@ public class ComponentType extends AbstractType implements CompositeTypeImplemen if ( original == null ) { return null; } + if ( compositeUserType != null ) { + return compositeUserType.replace( original, target, owner ); + } //if ( original == target ) return target; @@ -574,7 +613,7 @@ public class ComponentType extends AbstractType implements CompositeTypeImplemen @Override public boolean isMutable() { - return true; + return compositeUserType == null || compositeUserType.isMutable(); } @Override @@ -584,6 +623,9 @@ public class ComponentType extends AbstractType implements CompositeTypeImplemen if ( value == null ) { return null; } + else if ( compositeUserType != null ) { + return compositeUserType.disassemble( value ); + } else { Object[] values = getPropertyValues( value ); for ( int i = 0; i < propertyTypes.length; i++ ) { @@ -600,6 +642,9 @@ public class ComponentType extends AbstractType implements CompositeTypeImplemen if ( object == null ) { return null; } + else if ( compositeUserType != null ) { + return compositeUserType.assemble( object, owner ); + } else { Object[] values = (Object[]) object; Object[] assembled = new Object[values.length]; diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/CurrencyJavaType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/CurrencyJavaType.java index 38c12cbb17..50ea82344e 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/CurrencyJavaType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/CurrencyJavaType.java @@ -8,7 +8,9 @@ package org.hibernate.type.descriptor.java; import java.util.Currency; +import org.hibernate.dialect.Dialect; import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.jdbc.JdbcType; /** * Descriptor for {@link Currency} handling. @@ -53,4 +55,9 @@ public class CurrencyJavaType extends AbstractClassJavaType { } throw unknownWrap( value.getClass() ); } + + @Override + public long getDefaultSqlLength(Dialect dialect, JdbcType jdbcType) { + return 3; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/type/internal/CompositeUserTypeJavaTypeWrapper.java b/hibernate-core/src/main/java/org/hibernate/type/internal/CompositeUserTypeJavaTypeWrapper.java new file mode 100644 index 0000000000..7c4a99e6d0 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/type/internal/CompositeUserTypeJavaTypeWrapper.java @@ -0,0 +1,154 @@ +/* + * 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.type.internal; + +import java.io.Serializable; +import java.util.Comparator; + +import org.hibernate.SharedSessionContract; +import org.hibernate.annotations.Immutable; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.ImmutableMutabilityPlan; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.java.MutabilityPlan; +import org.hibernate.type.descriptor.java.MutabilityPlanExposer; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; +import org.hibernate.usertype.CompositeUserType; + +/** + * + * @author Christian Beikov + */ +public class CompositeUserTypeJavaTypeWrapper implements JavaType { + protected final CompositeUserType userType; + private final MutabilityPlan mutabilityPlan; + + private final Comparator comparator; + + public CompositeUserTypeJavaTypeWrapper(CompositeUserType userType) { + this.userType = userType; + + MutabilityPlan resolvedMutabilityPlan = null; + + if ( userType instanceof MutabilityPlanExposer ) { + //noinspection unchecked + resolvedMutabilityPlan = ( (MutabilityPlanExposer) userType ).getExposedMutabilityPlan(); + } + + if ( resolvedMutabilityPlan == null ) { + final Class jClass = userType.returnedClass(); + if ( jClass != null ) { + if ( jClass.getAnnotation( Immutable.class ) != null ) { + resolvedMutabilityPlan = ImmutableMutabilityPlan.instance(); + } + } + } + + if ( resolvedMutabilityPlan == null ) { + resolvedMutabilityPlan = new MutabilityPlanWrapper<>( userType ); + } + + this.mutabilityPlan = resolvedMutabilityPlan; + + if ( userType instanceof Comparator ) { + //noinspection unchecked + this.comparator = ( (Comparator) userType ); + } + else { + this.comparator = this::compare; + } + } + + private int compare(J first, J second) { + if ( userType.equals( first, second ) ) { + return 0; + } + return Comparator.comparing( userType::hashCode ).compare( first, second ); + } + + @Override + public MutabilityPlan getMutabilityPlan() { + return mutabilityPlan; + } + + @Override + public JdbcType getRecommendedJdbcType(JdbcTypeIndicators context) { + return null; + } + + @Override + public Comparator getComparator() { + return comparator; + } + + @Override + public int extractHashCode(J value) { + return userType.hashCode(value ); + } + + @Override + public boolean areEqual(J one, J another) { + return userType.equals( one, another ); + } + + @Override + public J fromString(CharSequence string) { + throw new UnsupportedOperationException( "No support for parsing UserType values from String: " + userType ); + } + + @Override + public X unwrap(J value, Class type, WrapperOptions options) { + assert value == null || userType.returnedClass().isInstance( value ); + + //noinspection unchecked + return (X) value; + } + + @Override + public J wrap(X value, WrapperOptions options) { +// assert value == null || userType.returnedClass().isInstance( value ); + + //noinspection unchecked + return (J) value; + } + + @Override + public Class getJavaTypeClass() { + return userType.returnedClass(); + } + + public static class MutabilityPlanWrapper implements MutabilityPlan { + private final CompositeUserType userType; + + public MutabilityPlanWrapper(CompositeUserType userType) { + this.userType = userType; + } + + @Override + public boolean isMutable() { + return userType.isMutable(); + } + + @Override + public J deepCopy(J value) { + //noinspection unchecked + return (J) userType.deepCopy( value ); + } + + @Override + public Serializable disassemble(J value, SharedSessionContract session) { + return userType.disassemble( value ); + } + + @Override + public J assemble(Serializable cached, SharedSessionContract session) { + //noinspection unchecked + return (J) userType.disassemble( cached ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/usertype/CompositeUserType.java b/hibernate-core/src/main/java/org/hibernate/usertype/CompositeUserType.java new file mode 100644 index 0000000000..4b30a588aa --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/usertype/CompositeUserType.java @@ -0,0 +1,142 @@ +/* + * 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.usertype; + +import java.io.Serializable; +import java.util.function.Supplier; + +import org.hibernate.HibernateException; +import org.hibernate.Incubating; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.metamodel.spi.EmbeddableInstantiator; + +/** + * A UserType that may be dereferenced in a query. + * This interface allows a custom type to define "properties". + * These need not necessarily correspond to physical JavaBeans + * style properties.
+ *
+ * A CompositeUserType may be used in almost every way + * that a component may be used. It may even contain many-to-one + * associations.
+ *
+ * Implementors must be immutable and must declare a public + * default constructor.
+ *
+ * Unlike UserType, cacheability does not depend upon + * serializability. Instead, assemble() and + * disassemble provide conversion to/from a cacheable + * representation. + *
+ * Properties are ordered by the order of their names + * i.e. they are alphabetically ordered, such that + * properties[i].name < properties[i + 1].name + * for all i >= 0. + */ +@Incubating +public interface CompositeUserType extends EmbeddableInstantiator { + + /** + * Get the value of a property. + * + * @param component an instance of class mapped by this "type" + * @param property the property index + * @return the property value + * @throws HibernateException + */ + Object getPropertyValue(J component, int property) throws HibernateException; + + @Override + J instantiate(Supplier values, SessionFactoryImplementor sessionFactory); + + /** + * The class that represents the embeddable mapping of the type. + * + * @return Class + */ + Class embeddable(); + + /** + * The class returned by {@code instantiate()}. + * + * @return Class + */ + Class returnedClass(); + + /** + * Compare two instances of the class mapped by this type for persistence "equality". + * Equality of the persistent state. + */ + boolean equals(Object x, Object y); + + /** + * Get a hashcode for the instance, consistent with persistence "equality" + */ + int hashCode(Object x); + + /** + * Return a deep copy of the persistent state, stopping at entities and at + * collections. It is not necessary to copy immutable objects, or null + * values, in which case it is safe to simply return the argument. + * + * @param value the object to be cloned, which may be null + * @return Object a copy + */ + Object deepCopy(Object value); + + /** + * Are objects of this type mutable? + * + * @return boolean + */ + boolean isMutable(); + + /** + * Transform the object into its cacheable representation. At the very least this + * method should perform a deep copy if the type is mutable. That may not be enough + * for some implementations, however; for example, associations must be cached as + * identifier values. (optional operation) + * + * @param value the object to be cached + * @return a cacheable representation of the object + */ + Serializable disassemble(Object value); + + /** + * Reconstruct an object from the cacheable representation. At the very least this + * method should perform a deep copy if the type is mutable. (optional operation) + * + * @param cached the object to be cached + * @param owner the owner of the cached object + * @return a reconstructed object from the cacheable representation + */ + Object assemble(Serializable cached, Object owner); + + /** + * During merge, replace the existing (target) value in the entity we are merging to + * with a new (original) value from the detached entity we are merging. For immutable + * objects, or null values, it is safe to simply return the first parameter. For + * mutable objects, it is safe to return a copy of the first parameter. For objects + * with component values, it might make sense to recursively replace component values. + * + * @param detached the value from the detached entity being merged + * @param managed the value in the managed entity + * + * @return the value to be merged + */ + Object replace(Object detached, Object managed, Object owner); + + @Override + default boolean isInstance(Object object, SessionFactoryImplementor sessionFactory) { + return returnedClass().isInstance( object ); + } + + @Override + default boolean isSameClass(Object object, SessionFactoryImplementor sessionFactory) { + return object.getClass().equals( returnedClass() ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/MonetaryAmountUserType.java b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/MonetaryAmountUserType.java index 0facf4f65e..8da361e5e3 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/MonetaryAmountUserType.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/MonetaryAmountUserType.java @@ -7,15 +7,13 @@ package org.hibernate.orm.test.sql.hand; import java.io.Serializable; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Types; +import java.math.BigDecimal; +import java.util.Currency; +import java.util.function.Supplier; import org.hibernate.HibernateException; -import org.hibernate.NotYetImplementedFor6Exception; -import org.hibernate.engine.spi.SharedSessionContractImplementor; -import org.hibernate.usertype.UserType; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.usertype.CompositeUserType; /** * This is a simple Hibernate custom mapping type for MonetaryAmount value types. @@ -23,17 +21,32 @@ import org.hibernate.usertype.UserType; * * @author Max & Christian */ -public class MonetaryAmountUserType implements UserType { - - private static final int[] SQL_TYPES = {Types.NUMERIC, Types.VARCHAR}; +public class MonetaryAmountUserType implements CompositeUserType { @Override - public int[] sqlTypes() { - return SQL_TYPES; + public Object getPropertyValue(MonetaryAmount component, int property) throws HibernateException { + switch ( property ) { + case 0: + return component.getCurrency(); + case 1: + return component.getValue(); + } + throw new HibernateException( "Illegal property index: " + property ); } @Override - public Class returnedClass() { + public MonetaryAmount instantiate(Supplier valueSupplier, SessionFactoryImplementor sessionFactory) { + final Object[] values = valueSupplier.get(); + return new MonetaryAmount( (BigDecimal) values[0], (Currency) values[1] ); + } + + @Override + public Class embeddable() { + return MonetaryAmountEmbeddable.class; + } + + @Override + public Class returnedClass() { return MonetaryAmount.class; } @@ -58,29 +71,6 @@ public class MonetaryAmountUserType implements UserType { return x.equals( y ); } - @Override - public Object nullSafeGet(ResultSet rs, int position, SharedSessionContractImplementor session, Object owner) throws SQLException { - // needs CompositeUserType - throw new NotYetImplementedFor6Exception( getClass() ); - } - - @Override - public void nullSafeSet( - PreparedStatement statement, - Object value, - int index, - SharedSessionContractImplementor session) throws HibernateException, SQLException { - if ( value == null ) { - statement.setNull( index, Types.NUMERIC ); - statement.setNull( index + 1, Types.VARCHAR ); - } - else { - MonetaryAmount currency = (MonetaryAmount) value; - statement.setBigDecimal( index, currency.getValue() ); - statement.setString( index + 1, currency.getCurrency().getCurrencyCode() ); - } - } - @Override public Serializable disassemble(Object value) throws HibernateException { return (Serializable) value; @@ -100,4 +90,9 @@ public class MonetaryAmountUserType implements UserType { public int hashCode(Object x) throws HibernateException { return x.hashCode(); } + + public static class MonetaryAmountEmbeddable { + private BigDecimal value; + private Currency currency; + } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/CustomSQLTestSupport.java b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/CustomSQLTestSupport.java index e95cbb46c7..31a96d192f 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/CustomSQLTestSupport.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/CustomSQLTestSupport.java @@ -135,7 +135,8 @@ public abstract class CustomSQLTestSupport extends BaseCoreFunctionalTestCase { public void testImageProperty() { Session s = openSession(); Transaction t = s.beginTransaction(); - byte[] photo = buildLongByteArray( 15000, true ); + // Make sure the last byte is non-zero as Sybase cuts that off + byte[] photo = buildLongByteArray( 14999, true ); ImageHolder holder = new ImageHolder( photo ); s.save( holder ); t.commit(); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/CustomStoredProcTestSupport.java b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/CustomStoredProcTestSupport.java index d6c55521b9..334af6bac7 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/CustomStoredProcTestSupport.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/CustomStoredProcTestSupport.java @@ -12,10 +12,9 @@ import java.util.List; import org.junit.Test; import org.hibernate.HibernateException; -import org.hibernate.query.Query; +import org.hibernate.procedure.ProcedureCall; import org.hibernate.Session; import org.hibernate.Transaction; -import org.hibernate.engine.query.ParameterRecognitionException; import org.hibernate.orm.test.sql.hand.Employment; import org.hibernate.orm.test.sql.hand.Organization; import org.hibernate.orm.test.sql.hand.Person; @@ -41,9 +40,10 @@ public abstract class CustomStoredProcTestSupport extends CustomSQLTestSupport { @Test public void testScalarStoredProcedure() throws HibernateException, SQLException { Session s = openSession(); - Query namedQuery = s.getNamedQuery( "simpleScalar" ); +// Query namedQuery = s.getNamedQuery( "simpleScalar" ); + ProcedureCall namedQuery = s.createNamedStoredProcedureQuery( "simpleScalar" ); namedQuery.setParameter( "number", 43 ); - List list = namedQuery.list(); + List list = namedQuery.getResultList(); Object o[] = ( Object[] ) list.get( 0 ); assertEquals( o[0], "getAll" ); assertEquals( o[1], Long.valueOf( 43 ) ); @@ -54,10 +54,11 @@ public abstract class CustomStoredProcTestSupport extends CustomSQLTestSupport { public void testParameterHandling() throws HibernateException, SQLException { Session s = openSession(); - Query namedQuery = s.getNamedQuery( "paramhandling" ); +// Query namedQuery = s.getNamedQuery( "paramhandling" ); + ProcedureCall namedQuery = s.createNamedStoredProcedureQuery( "paramhandling" ); namedQuery.setParameter( 1, 10 ); namedQuery.setParameter( 2, 20 ); - List list = namedQuery.list(); + List list = namedQuery.getResultList(); Object[] o = ( Object[] ) list.get( 0 ); assertEquals( o[0], Long.valueOf( 10 ) ); assertEquals( o[1], Long.valueOf( 20 ) ); @@ -69,11 +70,14 @@ public abstract class CustomStoredProcTestSupport extends CustomSQLTestSupport { inTransaction( session -> { try { - session.getNamedQuery( "paramhandling_mixed" ); + session.createNamedStoredProcedureQuery( "paramhandling_mixed" ); fail( "Expecting an exception" ); } - catch (ParameterRecognitionException expected) { - // expected outcome + catch (IllegalArgumentException expected) { + assertEquals( + "Cannot mix named parameter with positional parameter registrations", + expected.getMessage() + ); } catch (Exception other) { throw new AssertionError( "Expecting a ParameterRecognitionException, but encountered " + other.getClass().getSimpleName(), other ); @@ -95,9 +99,11 @@ public abstract class CustomStoredProcTestSupport extends CustomSQLTestSupport { s.persist( jboss ); s.persist( gavin ); s.persist( emp ); + s.flush(); - Query namedQuery = s.getNamedQuery( "selectAllEmployments" ); - List list = namedQuery.list(); +// Query namedQuery = s.getNamedQuery( "selectAllEmployments" ); + ProcedureCall namedQuery = s.createNamedStoredProcedureQuery( "selectAllEmployments" ); + List list = namedQuery.getResultList(); assertTrue( list.get( 0 ) instanceof Employment ); s.delete( emp ); s.delete( ifa ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/datadirect/oracle/StoredProcedures.hbm.xml b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/datadirect/oracle/StoredProcedures.hbm.xml index 39b17df40e..6611e1bb57 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/datadirect/oracle/StoredProcedures.hbm.xml +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/datadirect/oracle/StoredProcedures.hbm.xml @@ -38,11 +38,11 @@ - - - - - + + + + { call allEmployments() } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/db2/DB2CustomSQLTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/db2/DB2CustomSQLTest.java index 8395f0097e..da51f004da 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/db2/DB2CustomSQLTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/db2/DB2CustomSQLTest.java @@ -10,8 +10,6 @@ import org.hibernate.dialect.DB2Dialect; import org.hibernate.orm.test.sql.hand.custom.CustomStoredProcTestSupport; import org.hibernate.testing.RequiresDialect; -import org.hibernate.testing.orm.junit.NotImplementedYet; -import org.junit.Ignore; /** * Custom SQL tests for DB2 @@ -19,9 +17,6 @@ import org.junit.Ignore; * @author Max Rydahl Andersen */ @RequiresDialect( DB2Dialect.class ) -// todo (6.0): needs a composite user type mechanism e.g. by providing a custom embeddable strategy or istantiator -@Ignore( "Missing support for composite user types" ) -@NotImplementedYet public class DB2CustomSQLTest extends CustomStoredProcTestSupport { public String[] getMappings() { return new String[] { "sql/hand/custom/db2/Mappings.hbm.xml" }; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/db2/Mappings.hbm.xml b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/db2/Mappings.hbm.xml index 02264b58ce..f61444e919 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/db2/Mappings.hbm.xml +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/db2/Mappings.hbm.xml @@ -56,10 +56,12 @@ - - - - + + + + + + INSERT INTO EMPLOYMENT @@ -164,13 +166,11 @@ - - - - - - + + + + @@ -209,12 +209,10 @@ - - - - - + + + { call selectAllEmployments() } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/derby/DerbyCustomSQLTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/derby/DerbyCustomSQLTest.java index 3272d7b3d1..d298b0077b 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/derby/DerbyCustomSQLTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/derby/DerbyCustomSQLTest.java @@ -10,16 +10,11 @@ import org.hibernate.dialect.DerbyDialect; import org.hibernate.orm.test.sql.hand.custom.CustomStoredProcTestSupport; import org.hibernate.testing.RequiresDialect; -import org.hibernate.testing.orm.junit.NotImplementedYet; -import org.junit.Ignore; /** * @author Andrea Boriero */ @RequiresDialect(DerbyDialect.class) -// todo (6.0): needs a composite user type mechanism e.g. by providing a custom embeddable strategy or istantiator -@Ignore( "Missing support for composite user types" ) -@NotImplementedYet public class DerbyCustomSQLTest extends CustomStoredProcTestSupport { public String[] getMappings() { return new String[] {"sql/hand/custom/derby/Mappings.hbm.xml"}; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/derby/DerbyStoreProcedures.java b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/derby/DerbyStoreProcedures.java index ef1edad4c4..7cd2b2eed9 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/derby/DerbyStoreProcedures.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/derby/DerbyStoreProcedures.java @@ -22,7 +22,7 @@ public class DerbyStoreProcedures { PreparedStatement statement = conn.prepareStatement( "select EMPLOYEE, EMPLOYER, STARTDATE, ENDDATE," + - " REGIONCODE, EMPID, 'VALUE', CURRENCY" + + " REGIONCODE, EMPID, \"VALUE\", CURRENCY" + " FROM EMPLOYMENT" ); resultSets[0] = statement.executeQuery(); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/derby/Mappings.hbm.xml b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/derby/Mappings.hbm.xml index 6ca32247bc..b16784a705 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/derby/Mappings.hbm.xml +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/derby/Mappings.hbm.xml @@ -56,10 +56,12 @@ - - - - + + + + + + INSERT INTO EMPLOYMENT @@ -165,12 +167,10 @@ - - - - - + + + @@ -209,12 +209,10 @@ - - - - - + + + { call selectAllEmployments() } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/mysql/Mappings.hbm.xml b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/mysql/Mappings.hbm.xml index 8cd4fbdf24..8e04918cc4 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/mysql/Mappings.hbm.xml +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/mysql/Mappings.hbm.xml @@ -57,10 +57,12 @@ - - - - + + + + + + INSERT INTO Employment @@ -163,12 +165,10 @@ - - - - - + + + @@ -206,13 +206,10 @@ - - - - - - + + + { call selectAllEmployments() } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/mysql/MySQLCustomSQLTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/mysql/MySQLCustomSQLTest.java index 4a76e9fdb3..0e17e0dddb 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/mysql/MySQLCustomSQLTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/mysql/MySQLCustomSQLTest.java @@ -7,11 +7,11 @@ package org.hibernate.orm.test.sql.hand.custom.mysql; import org.hibernate.dialect.MySQLDialect; +import org.hibernate.dialect.TiDBDialect; import org.hibernate.orm.test.sql.hand.custom.CustomStoredProcTestSupport; import org.hibernate.testing.RequiresDialect; -import org.hibernate.testing.orm.junit.NotImplementedYet; -import org.junit.Ignore; +import org.hibernate.testing.SkipForDialect; /** * Custom SQL tests for MySQL @@ -19,9 +19,7 @@ import org.junit.Ignore; * @author Gavin King */ @RequiresDialect( MySQLDialect.class ) -// todo (6.0): needs a composite user type mechanism e.g. by providing a custom embeddable strategy or istantiator -@Ignore( "Missing support for composite user types" ) -@NotImplementedYet +@SkipForDialect(value = TiDBDialect.class, comment = "TiDB doesn't support stored procedures") public class MySQLCustomSQLTest extends CustomStoredProcTestSupport { public String[] getMappings() { return new String[] { "sql/hand/custom/mysql/Mappings.hbm.xml" }; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/oracle/Mappings.hbm.xml b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/oracle/Mappings.hbm.xml index 81c49f78cf..6f073ed309 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/oracle/Mappings.hbm.xml +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/oracle/Mappings.hbm.xml @@ -57,10 +57,12 @@ - - - - + + + + + + INSERT INTO EMPLOYMENT @@ -157,13 +159,10 @@ - - - - - - + + + diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/oracle/OracleCustomSQLTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/oracle/OracleCustomSQLTest.java index 36ed3f8e4b..d5e6cba632 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/oracle/OracleCustomSQLTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/oracle/OracleCustomSQLTest.java @@ -10,8 +10,6 @@ import org.hibernate.dialect.OracleDialect; import org.hibernate.orm.test.sql.hand.custom.CustomStoredProcTestSupport; import org.hibernate.testing.RequiresDialect; -import org.hibernate.testing.orm.junit.NotImplementedYet; -import org.junit.Ignore; /** * Custom SQL tests for Oracle @@ -19,9 +17,6 @@ import org.junit.Ignore; * @author Gavin King */ @RequiresDialect( OracleDialect.class ) -// todo (6.0): needs a composite user type mechanism e.g. by providing a custom embeddable strategy or istantiator -@Ignore( "Missing support for composite user types" ) -@NotImplementedYet public class OracleCustomSQLTest extends CustomStoredProcTestSupport { public String[] getMappings() { return new String[] { "sql/hand/custom/oracle/Mappings.hbm.xml", "sql/hand/custom/oracle/StoredProcedures.hbm.xml" }; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/oracle/StoredProcedures.hbm.xml b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/oracle/StoredProcedures.hbm.xml index 82b0fa94f7..0a844c2bb7 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/oracle/StoredProcedures.hbm.xml +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/oracle/StoredProcedures.hbm.xml @@ -40,10 +40,8 @@ - - - - + + { ? = call allEmployments() } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/sqlserver/Mappings.hbm.xml b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/sqlserver/Mappings.hbm.xml index 78fae509b3..4e1ad8934e 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/sqlserver/Mappings.hbm.xml +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/sqlserver/Mappings.hbm.xml @@ -56,10 +56,12 @@ - - - - + + + + + + INSERT INTO EMPLOYMENT @@ -70,11 +72,11 @@ DELETE FROM EMPLOYMENT WHERE EMPID=? - - + + - + INSERT INTO TEXTHOLDER @@ -85,11 +87,11 @@ DELETE FROM TEXTHOLDER WHERE ID=? - - + + - + INSERT INTO IMAGEHOLDER @@ -163,13 +165,11 @@ - - - - - - + + + + @@ -207,13 +207,11 @@ - - - - - - + + + + { call selectAllEmployments() } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/sqlserver/SQLServerCustomSQLTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/sqlserver/SQLServerCustomSQLTest.java index 727a73b8b2..6324e53d01 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/sqlserver/SQLServerCustomSQLTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/sqlserver/SQLServerCustomSQLTest.java @@ -10,8 +10,6 @@ import org.hibernate.dialect.SQLServerDialect; import org.hibernate.orm.test.sql.hand.custom.CustomStoredProcTestSupport; import org.hibernate.testing.RequiresDialect; -import org.hibernate.testing.orm.junit.NotImplementedYet; -import org.junit.Ignore; /** * Custom SQL tests for SQLServer @@ -19,9 +17,6 @@ import org.junit.Ignore; * @author Gail Badner */ @RequiresDialect( SQLServerDialect.class ) -// todo (6.0): needs a composite user type mechanism e.g. by providing a custom embeddable strategy or istantiator -@Ignore( "Missing support for composite user types" ) -@NotImplementedYet public class SQLServerCustomSQLTest extends CustomStoredProcTestSupport { public String[] getMappings() { return new String[] { "sql/hand/custom/sqlserver/Mappings.hbm.xml" }; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/sybase/Mappings.hbm.xml b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/sybase/Mappings.hbm.xml index c5e233f289..4eb4641a7c 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/sybase/Mappings.hbm.xml +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/sybase/Mappings.hbm.xml @@ -56,10 +56,12 @@ - - - - + + + + + + INSERT INTO EMPLOYMENT @@ -70,11 +72,11 @@ DELETE FROM EMPLOYMENT WHERE EMPID=? - - + + - + INSERT INTO TEXTHOLDER @@ -85,11 +87,11 @@ DELETE FROM TEXTHOLDER WHERE ID=? - - + + - + INSERT INTO IMAGEHOLDER @@ -163,13 +165,11 @@ - - - - - - + + + + @@ -207,13 +207,11 @@ - - - - - - + + + + { call selectAllEmployments() } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/sybase/SybaseCustomSQLTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/sybase/SybaseCustomSQLTest.java index 4e091df45d..857ca020b6 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/sybase/SybaseCustomSQLTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/custom/sybase/SybaseCustomSQLTest.java @@ -10,8 +10,6 @@ import org.hibernate.dialect.SybaseDialect; import org.hibernate.orm.test.sql.hand.custom.CustomStoredProcTestSupport; import org.hibernate.testing.RequiresDialect; -import org.hibernate.testing.orm.junit.NotImplementedYet; -import org.junit.Ignore; /** * Custom SQL tests for Sybase dialects @@ -19,9 +17,6 @@ import org.junit.Ignore; * @author Gavin King */ @RequiresDialect( { SybaseDialect.class }) -// todo (6.0): needs a composite user type mechanism e.g. by providing a custom embeddable strategy or istantiator -@Ignore( "Missing support for composite user types" ) -@NotImplementedYet public class SybaseCustomSQLTest extends CustomStoredProcTestSupport { public String[] getMappings() { return new String[] { "sql/hand/custom/sybase/Mappings.hbm.xml" }; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/query/NativeSQLQueries.hbm.xml b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/query/NativeSQLQueries.hbm.xml index 5964f5905b..9df010aac3 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/query/NativeSQLQueries.hbm.xml +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/query/NativeSQLQueries.hbm.xml @@ -58,12 +58,12 @@ - + + + + + + @@ -129,14 +129,14 @@ - + - + @@ -221,13 +221,13 @@ - + SELECT * FROM EMPLOYMENT - - + + SELECT * FROM EMPLOYMENT, PERSON @@ -253,24 +253,22 @@ - - - - + + SELECT org.ORGID as orgid, org.NAME as name, - emp.EMPLOYER as employer, emp.EMPID as empid, emp.EMPLOYEE as employee, emp.EMPLOYER as employer, emp.STARTDATE as xstartDate, emp.ENDDATE as endDate, emp.REGIONCODE as regionCode, - emp.AMOUNT as AMOUNT, + emp.AMOUNT as AMOUNT1, emp.CURRENCY as CURRENCY FROM ORGANIZATION org LEFT OUTER JOIN EMPLOYMENT emp ON org.ORGID = emp.EMPLOYER + ORDER BY org.NAME DESC @@ -278,7 +276,6 @@ SELECT org.ORGID as orgid, org.NAME as name, - emp.EMPLOYER as employer, emp.EMPID as empid, emp.EMPLOYEE as employee, emp.EMPLOYER as employer, diff --git a/hibernate-core/src/test_legacy/org/hibernate/test/sql/hand/query/NativeSQLQueriesTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/query/NativeSQLQueriesTest.java similarity index 92% rename from hibernate-core/src/test_legacy/org/hibernate/test/sql/hand/query/NativeSQLQueriesTest.java rename to hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/query/NativeSQLQueriesTest.java index 99331d7bc9..c6e1372741 100644 --- a/hibernate-core/src/test_legacy/org/hibernate/test/sql/hand/query/NativeSQLQueriesTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/hand/query/NativeSQLQueriesTest.java @@ -4,12 +4,17 @@ * 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.test.sql.hand.query; +package org.hibernate.orm.test.sql.hand.query; import java.math.BigDecimal; import java.math.BigInteger; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.SQLException; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.IdentityHashMap; import java.util.Iterator; import java.util.List; import java.util.Locale; @@ -35,13 +40,13 @@ import org.hibernate.orm.test.sql.hand.Speech; import org.hibernate.orm.test.sql.hand.TextHolder; import org.hibernate.query.NativeQuery; import org.hibernate.query.Query; +import org.hibernate.query.ResultListTransformer; import org.hibernate.transform.ResultTransformer; import org.hibernate.transform.Transformers; import org.hibernate.type.StandardBasicTypes; import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.FailureExpected; -import org.hibernate.testing.orm.junit.NotImplementedYet; import org.hibernate.testing.orm.junit.RequiresDialect; import org.hibernate.testing.orm.junit.ServiceRegistry; import org.hibernate.testing.orm.junit.SessionFactory; @@ -73,7 +78,6 @@ import static org.junit.jupiter.api.Assertions.fail; xmlMappings = { "org/hibernate/orm/test/sql/hand/query/NativeSQLQueries.hbm.xml" } ) @SessionFactory -@NotImplementedYet( strict = false, reason = "needs a composite user type mechanism e.g. by providing a custom embeddable strategy or instantiator" ) public class NativeSQLQueriesTest { protected String getOrganizationFetchJoinEmploymentSQL() { @@ -99,7 +103,7 @@ public class NativeSQLQueriesTest { } protected String getEmploymentSQLMixedScalarEntity() { - return "SELECT e.*, e.employer as employerid FROM EMPLOYMENT e" ; + return "SELECT e.*, e.EMPLOYER as employerid FROM EMPLOYMENT e" ; } protected String getOrgEmpRegionSQL() { @@ -190,6 +194,7 @@ public class NativeSQLQueriesTest { session.persist(jboss); session.persist(gavin); session.persist(emp); + session.flush(); List l = session.createNativeQuery( getOrgEmpRegionSQL() ) .addEntity("org", Organization.class) @@ -203,7 +208,7 @@ public class NativeSQLQueriesTest { .addJoin("emp", "org.employments") .addJoin("pers", "emp.employee") .list(); - assertEquals( l.size(), 1 ); + assertEquals( 1, l.size() ); } ); @@ -214,14 +219,21 @@ public class NativeSQLQueriesTest { " left outer join EMPLOYMENT emp on org.ORGID = emp.EMPLOYER, ORGANIZATION org2" ) .addEntity("org", Organization.class) .addJoin("emp", "org.employments") - .setResultTransformer(new ResultTransformer() { + .setResultListTransformer( new ResultListTransformer() { @Override - public Object transformTuple(Object[] tuple, String[] aliases) { - return tuple[0]; + public List transformList(List list) { + List result = new ArrayList<>( list.size() ); + Map distinct = new IdentityHashMap<>(); + for ( Object entity : list ) { + if ( distinct.put( entity, entity ) == null ) { + result.add( entity ); + } + } + return result; } - }) + } ) .list(); - assertEquals( l.size(), 2 ); + assertEquals( 2, l.size() ); } ); @@ -248,12 +260,13 @@ public class NativeSQLQueriesTest { session.persist(jboss); session.persist(gavin); session.persist(emp); + session.flush(); List l = session.createNativeQuery( getOrgEmpRegionSQL(), "org-emp-regionCode" ).list(); - assertEquals( l.size(), 2 ); + assertEquals( 2, l.size() ); l = session.createNativeQuery( getOrgEmpPersonSQL(), "org-emp-person" ).list(); - assertEquals( l.size(), 1 ); + assertEquals( 1, l.size() ); session.delete(emp); session.delete(gavin); @@ -276,12 +289,13 @@ public class NativeSQLQueriesTest { session.persist(jboss); session.persist(gavin); session.persist(emp); + session.flush(); List l = session.createNativeQuery( getOrgEmpRegionSQL(), "org-emp-regionCode", Object[].class ).list(); - assertEquals( l.size(), 2 ); + assertEquals( 2, l.size() ); l = session.createNativeQuery( getOrgEmpPersonSQL(), "org-emp-person", Object[].class ).list(); - assertEquals( l.size(), 1 ); + assertEquals( 1, l.size() ); session.delete(emp); session.delete(gavin); @@ -410,20 +424,18 @@ public class NativeSQLQueriesTest { List list = sqlQuery.list(); assertEquals( 2,list.size() ); Map m = (Map) list.get(0); - assertEquals( 2, m.size() ); + assertEquals( 1, m.size() ); assertTrue( m.containsKey("org") ); - assertTrue( m.containsKey("emp") ); assertClassAssignability( m.get("org").getClass(), Organization.class ); if ( jboss.getId() == ( (Organization) m.get("org") ).getId() ) { - assertClassAssignability( m.get("emp").getClass(), Employment.class ); + assertTrue( Hibernate.isInitialized( ( (Organization) m.get("org") ).getEmployments() ) ); } Map m2 = (Map) list.get(1); - assertEquals( 2, m.size() ); + assertEquals( 1, m.size() ); assertTrue( m2.containsKey("org") ); - assertTrue( m2.containsKey("emp") ); assertClassAssignability( m2.get("org").getClass(), Organization.class ); - if ( jboss.getId() == ( (Organization) m2.get("org") ).getId() ) { - assertClassAssignability( m2.get("emp").getClass(), Employment.class ); + if ( ifa.getId() == ( (Organization) m2.get("org") ).getId() ) { + assertTrue( Hibernate.isInitialized( ( (Organization) m2.get("org") ).getEmployments() ) ); } } ); @@ -643,6 +655,7 @@ public class NativeSQLQueriesTest { Dimension d = new Dimension(45, 10); enterprise.setDimensions( d ); session.save( enterprise ); + session.flush(); Object[] result = (Object[]) session.getNamedQuery( "spaceship" ).uniqueResult(); assertEquals( 3, result.length, "expecting 3 result values" ); enterprise = ( SpaceShip ) result[0]; @@ -674,7 +687,6 @@ public class NativeSQLQueriesTest { String sql = "SELECT org.ORGID as orgid," + " org.NAME as name," + - " emp.EMPLOYER as employer," + " emp.EMPID as empid," + " emp.EMPLOYEE as employee," + " emp.EMPLOYER as employer," + @@ -823,7 +835,6 @@ public class NativeSQLQueriesTest { ); } - @SkipForDialect(dialectClass = AbstractHANADialect.class, reason = "On HANA, this returns an clob for the text column which doesn't get mapped to a String") @Test public void testTextTypeInSQLQuery(SessionFactoryScope scope) { String description = buildLongString( 15000, 'a' ); @@ -835,18 +846,31 @@ public class NativeSQLQueriesTest { scope.inTransaction( session -> { - String descriptionRead = ( String ) session.createNativeQuery( getDescriptionsSQL() ) - .uniqueResult(); + Object result = session.createNativeQuery( getDescriptionsSQL() ).uniqueResult(); + + String descriptionRead; + if ( result instanceof String ) { + descriptionRead = (String) result; + } + else { + Clob clob = (Clob) result; + try { + descriptionRead = clob.getSubString( 1L, (int) clob.length() ); + } + catch (SQLException e) { + throw new RuntimeException( e ); + } + } assertEquals( description, descriptionRead ); session.delete( holder ); } ); } - @SkipForDialect(dialectClass = AbstractHANADialect.class, reason = "On HANA, this returns a blob for the image column which doesn't get mapped to a byte[]") @Test public void testImageTypeInSQLQuery(SessionFactoryScope scope) { - byte[] photo = buildLongByteArray( 15000, true ); + // Make sure the last byte is non-zero as Sybase cuts that off + byte[] photo = buildLongByteArray( 14999, true ); ImageHolder holder = new ImageHolder( photo ); scope.inTransaction( @@ -855,8 +879,20 @@ public class NativeSQLQueriesTest { scope.inTransaction( session -> { - byte[] photoRead = ( byte[] ) session.createNativeQuery( getPhotosSQL() ) - .uniqueResult(); + Object result = session.createNativeQuery( getPhotosSQL() ).uniqueResult(); + byte[] photoRead; + if ( result instanceof byte[] ) { + photoRead = (byte[]) result; + } + else { + Blob blob = (Blob) result; + try { + photoRead = blob.getBytes( 1L, (int) blob.length() ); + } + catch (SQLException e) { + throw new RuntimeException( e ); + } + } assertTrue( Arrays.equals( photo, photoRead ) ); session.delete( holder ); } diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/AbstractCollectionMetadataGenerator.java b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/AbstractCollectionMetadataGenerator.java index 9e089e78e2..9b2f841e4f 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/AbstractCollectionMetadataGenerator.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/AbstractCollectionMetadataGenerator.java @@ -335,6 +335,7 @@ public abstract class AbstractCollectionMetadataGenerator extends AbstractMetada valueMetadataGenerator.addValue( entity, component.getProperty( auditedPropertyName ).getValue(), + component.getProperty( auditedPropertyName ).getPropertyAccessStrategy(), componentMapper, prefix, context.getEntityMappingData(), @@ -351,6 +352,7 @@ public abstract class AbstractCollectionMetadataGenerator extends AbstractMetada valueMetadataGenerator.addValue( entity, component.getProperty( auditedPropertyName ).getValue(), + component.getProperty( auditedPropertyName ).getPropertyAccessStrategy(), componentMapper, context.getReferencingEntityName(), context.getEntityMappingData(), diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/AuditMetadataGenerator.java b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/AuditMetadataGenerator.java index 10e7966ff2..49d56c9042 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/AuditMetadataGenerator.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/AuditMetadataGenerator.java @@ -105,6 +105,7 @@ public final class AuditMetadataGenerator extends AbstractMetadataGenerator { valueMetadataGenerator.addValue( attributeContainer, property.getValue(), + property.getPropertyAccessStrategy(), currentMapper, entityName, mappingData, @@ -370,6 +371,7 @@ public final class AuditMetadataGenerator extends AbstractMetadataGenerator { valueMetadataGenerator.addValue( entity, propertyAuditingData.getValue(), + null, currentMapper, entityName, mappingData, 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 0071193f1c..bd1a9c7ae0 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,10 @@ import org.hibernate.envers.internal.entities.mapper.CompositeMapperBuilder; 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.spi.EmbeddableInstantiator; +import org.hibernate.resource.beans.spi.ManagedBeanRegistry; +import org.hibernate.usertype.CompositeUserType; /** * Generates metadata for components. @@ -46,19 +50,50 @@ public final class ComponentMetadataGenerator extends AbstractMetadataGenerator EntityMappingData mappingData, boolean firstPass) { final Component propComponent = (Component) value; - + final Class instantiatorClass; + if ( propComponent.getCustomInstantiator() != null ) { + instantiatorClass = propComponent.getCustomInstantiator(); + } + else { + instantiatorClass = null; + } + final EmbeddableInstantiator instantiator; + if ( propComponent.getCustomInstantiator() != null ) { + instantiator = getMetadataBuildingContext().getBootstrapContext() + .getServiceRegistry() + .getService( ManagedBeanRegistry.class ) + .getBean( propComponent.getCustomInstantiator() ) + .getBeanInstance(); + } + else if ( propComponent.getTypeName() != null ) { + final CompositeUserType compositeUserType = (CompositeUserType) getMetadataBuildingContext().getBootstrapContext() + .getServiceRegistry() + .getService( ManagedBeanRegistry.class ) + .getBean( + getMetadataBuildingContext().getBootstrapContext() + .getClassLoaderAccess() + .classForName( propComponent.getTypeName() ) + ) + .getBeanInstance(); + instantiator = new EmbeddableCompositeUserTypeInstantiator( compositeUserType ); + } + else { + instantiator = null; + } final CompositeMapperBuilder componentMapper = mapper.addComponent( propertyAuditingData.resolvePropertyData(), ClassLoaderAccessHelper.loadClass( getMetadataBuildingContext(), getClassNameForComponent( propComponent ) - ) + ), + instantiator ); // The property auditing data must be for a component. final ComponentAuditingData componentAuditingData = (ComponentAuditingData) propertyAuditingData; // Adding all properties of the component + propComponent.sortProperties(); final Iterator properties = propComponent.getPropertyIterator(); while ( properties.hasNext() ) { final Property property = properties.next(); @@ -71,6 +106,7 @@ public final class ComponentMetadataGenerator extends AbstractMetadataGenerator valueGenerator.addValue( attributeContainer, property.getValue(), + property.getPropertyAccessStrategy(), componentMapper, entityName, mappingData, diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/ValueMetadataGenerator.java b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/ValueMetadataGenerator.java index 29fa4fbfa9..4053088cf2 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/ValueMetadataGenerator.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/ValueMetadataGenerator.java @@ -18,6 +18,7 @@ import org.hibernate.envers.internal.tools.MappingTools; import org.hibernate.mapping.Collection; import org.hibernate.mapping.OneToOne; import org.hibernate.mapping.Value; +import org.hibernate.property.access.spi.PropertyAccessStrategy; import org.hibernate.type.BasicType; import org.hibernate.type.CollectionType; import org.hibernate.type.ComponentType; @@ -52,6 +53,7 @@ public class ValueMetadataGenerator extends AbstractMetadataGenerator { public void addValue( AttributeContainer attributeContainer, Value value, + PropertyAccessStrategy propertyAccessStrategy, CompositeMapperBuilder currentMapper, String entityName, EntityMappingData mappingData, @@ -63,6 +65,7 @@ public class ValueMetadataGenerator extends AbstractMetadataGenerator { addValueInFirstPass( attributeContainer, value, + propertyAccessStrategy, currentMapper, entityName, mappingData, @@ -75,6 +78,7 @@ public class ValueMetadataGenerator extends AbstractMetadataGenerator { addValueInSecondPass( attributeContainer, value, + propertyAccessStrategy, currentMapper, entityName, mappingData, @@ -88,6 +92,7 @@ public class ValueMetadataGenerator extends AbstractMetadataGenerator { private void addValueInFirstPass( AttributeContainer attributeContainer, Value value, + PropertyAccessStrategy propertyAccessStrategy, CompositeMapperBuilder currentMapper, String entityName, EntityMappingData mappingData, @@ -95,6 +100,7 @@ public class ValueMetadataGenerator extends AbstractMetadataGenerator { boolean insertable, boolean processModifiedFlag) { final Type type = value.getType(); + propertyAuditingData.setPropertyAccessStrategy( propertyAccessStrategy ); if ( type instanceof BasicType ) { basicMetadataGenerator.addBasic( @@ -134,6 +140,7 @@ public class ValueMetadataGenerator extends AbstractMetadataGenerator { private void addValueInSecondPass( AttributeContainer attributeContainer, Value value, + PropertyAccessStrategy propertyAccessStrategy, CompositeMapperBuilder currentMapper, String entityName, EntityMappingData mappingData, diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/reader/AuditedPropertiesReader.java b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/reader/AuditedPropertiesReader.java index f5f8b66b1a..aca31bb7c9 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/reader/AuditedPropertiesReader.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/reader/AuditedPropertiesReader.java @@ -9,6 +9,7 @@ package org.hibernate.envers.configuration.internal.metadata.reader; import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; @@ -116,6 +117,10 @@ public class AuditedPropertiesReader { } public void read() { + read(null); + } + + public void read(Audited allClassAudited) { // First reading the access types for the persistent properties. readPersistentPropertiesAccess(); @@ -126,10 +131,33 @@ public class AuditedPropertiesReader { // Retrieve classes and properties that are explicitly marked for auditing process by any superclass // of currently mapped entity or itself. final XClass clazz = persistentPropertiesSource.getXClass(); - readAuditOverrides( clazz ); - - // Adding all properties from the given class. - addPropertiesFromClass( clazz ); + if ( persistentPropertiesSource.hasCompositeUserType() ) { + for ( String propertyName : fieldAccessedPersistentProperties ) { + final Property property = persistentPropertiesSource.getProperty( propertyName ); + final Value propertyValue = property.getValue(); + if ( propertyValue instanceof Component ) { + this.addFromComponentProperty( property, "field", (Component) propertyValue, allClassAudited ); + } + else { + this.addFromNotComponentProperty( property, "field", allClassAudited ); + } + } + for ( String propertyName : propertyAccessedPersistentProperties.keySet() ) { + final Property property = persistentPropertiesSource.getProperty( propertyName ); + final Value propertyValue = property.getValue(); + if ( propertyValue instanceof Component ) { + this.addFromComponentProperty( property, "property", (Component) propertyValue, allClassAudited ); + } + else { + this.addFromNotComponentProperty( property, "property", allClassAudited ); + } + } + } + else { + readAuditOverrides( clazz ); + // Adding all properties from the given class. + addPropertiesFromClass( clazz ); + } } } @@ -475,7 +503,7 @@ public class AuditedPropertiesReader { componentData, propertyNamePrefix + MappingTools.createComponentPrefix( property.getName() ) ); - audPropReader.read(); + audPropReader.read( allClassAudited ); if ( isAudited ) { // Now we know that the property is audited @@ -554,6 +582,82 @@ public class AuditedPropertiesReader { return true; } + private void addFromComponentProperty( + Property property, + String accessType, + Component propertyValue, + Audited allClassAudited) { + final ComponentAuditingData componentData = new ComponentAuditingData(); + final boolean isAudited = fillPropertyData( property, componentData, accessType, allClassAudited ); + + final PersistentPropertiesSource componentPropertiesSource; + if ( propertyValue.isDynamic() ) { + final XClass xClass = metadataBuildingContext.getReflectionManager().toXClass( Map.class ); + componentPropertiesSource = PersistentPropertiesSource.forComponent( propertyValue, xClass, true ); + } + else { + componentPropertiesSource = PersistentPropertiesSource.forComponent( metadataBuildingContext, propertyValue ); + } + + final ComponentAuditedPropertiesReader audPropReader = new ComponentAuditedPropertiesReader( + metadataBuildingContext, + componentPropertiesSource, + componentData, + propertyNamePrefix + MappingTools.createComponentPrefix( property.getName() ) + ); + audPropReader.read( allClassAudited ); + + if ( isAudited ) { + // Now we know that the property is audited + auditedPropertiesHolder.addPropertyAuditingData( property.getName(), componentData ); + } + } + + private void addFromNotComponentProperty(Property property, String accessType, Audited allClassAudited) { + final PropertyAuditingData propertyData = new PropertyAuditingData(); + final boolean isAudited = fillPropertyData( property, propertyData, accessType, allClassAudited ); + + if ( isAudited ) { + // Now we know that the property is audited + auditedPropertiesHolder.addPropertyAuditingData( property.getName(), propertyData ); + } + } + + + /** + * Checks if a property is audited and if yes, fills all of its data. + * + * @param property Property to check. + * @param propertyData Property data, on which to set this property's modification store. + * @param accessType Access type for the property. + * + * @return False if this property is not audited. + */ + private boolean fillPropertyData( + Property property, + PropertyAuditingData propertyData, + String accessType, + Audited allClassAudited) { + + final String propertyName = propertyNamePrefix + property.getName(); + final String modifiedFlagsSuffix = metadataBuildingContext.getConfiguration().getModifiedFlagsSuffix(); + if ( !this.checkAudited( property, propertyData,propertyName, allClassAudited, modifiedFlagsSuffix ) ) { + return false; + } + + propertyData.setName( propertyName ); + propertyData.setBeanName( property.getName() ); + propertyData.setAccessType( accessType ); + + propertyData.setJoinTable( DEFAULT_AUDIT_JOIN_TABLE ); + if ( !processPropertyAuditingOverrides( property, propertyData ) ) { + // not audited due to AuditOverride annotation + return false; + } + + return true; + } + private void validateLobMappingSupport(XProperty property) { // HHH-9834 - Sanity check try { @@ -610,6 +714,30 @@ public class AuditedPropertiesReader { } } + protected boolean checkAudited( + Property property, + PropertyAuditingData propertyData, String propertyName, + Audited allClassAudited, String modifiedFlagSuffix) { + // Checking if this property is explicitly audited or if all properties are. + if ( allClassAudited != null ) { + propertyData.setRelationTargetAuditMode( allClassAudited.targetAuditMode() ); + propertyData.setRelationTargetNotFoundAction( + allClassAudited == null ? + RelationTargetNotFoundAction.DEFAULT : + allClassAudited.targetNotFoundAction() + ); + propertyData.setUsingModifiedFlag( checkUsingModifiedFlag( allClassAudited ) ); + propertyData.setModifiedFlagName( ModifiedColumnNameResolver.getName( propertyName, modifiedFlagSuffix ) ); + if ( !StringTools.isEmpty( allClassAudited.modifiedColumnName() ) ) { + propertyData.setExplicitModifiedFlagName( allClassAudited.modifiedColumnName() ); + } + return true; + } + else { + return false; + } + } + protected boolean checkUsingModifiedFlag(Audited aud) { // HHH-10468 if ( metadataBuildingContext.getConfiguration().hasSettingForUseModifiedFlag() ) { @@ -730,6 +858,24 @@ public class AuditedPropertiesReader { return true; } + private boolean processPropertyAuditingOverrides(Property property, PropertyAuditingData propertyData) { + // Only components register audit overrides, classes will have no entries. + for ( AuditOverrideData override : auditedPropertiesHolder.getAuditingOverrides() ) { + if ( property.getName().equals( override.getName() ) ) { + // the override applies to this property + if ( !override.isAudited() ) { + return false; + } + else { + if ( override.getAuditJoinTableData() != null ) { + propertyData.setJoinTable( override.getAuditJoinTableData() ); + } + } + } + } + return true; + } + protected boolean isOverriddenNotAudited(XProperty property) { return overriddenNotAuditedProperties.contains( property ); } diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/reader/PersistentPropertiesSource.java b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/reader/PersistentPropertiesSource.java index 5f9d93160d..c9c33c5f5d 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/reader/PersistentPropertiesSource.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/reader/PersistentPropertiesSource.java @@ -32,6 +32,8 @@ public interface PersistentPropertiesSource { boolean isDynamicComponent(); + boolean hasCompositeUserType(); + /** * Get a persistent properties source for a persistent class. * @@ -60,6 +62,11 @@ public interface PersistentPropertiesSource { public boolean isDynamicComponent() { return false; } + + @Override + public boolean hasCompositeUserType() { + return false; + } }; } @@ -115,6 +122,11 @@ public interface PersistentPropertiesSource { public boolean isDynamicComponent() { return dynamic; } + + @Override + public boolean hasCompositeUserType() { + return component.getTypeName() != null; + } }; } } diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/reader/PropertyAuditingData.java b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/reader/PropertyAuditingData.java index 7dc16e4a75..49c80d084d 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/reader/PropertyAuditingData.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/reader/PropertyAuditingData.java @@ -20,6 +20,7 @@ import org.hibernate.envers.RelationTargetNotFoundAction; import org.hibernate.envers.internal.entities.PropertyData; import org.hibernate.envers.internal.tools.StringTools; import org.hibernate.mapping.Value; +import org.hibernate.property.access.spi.PropertyAccessStrategy; import org.hibernate.type.Type; /** @@ -50,6 +51,7 @@ public class PropertyAuditingData { private Value value; private Type propertyType; private Type virtualPropertyType; + private PropertyAccessStrategy propertyAccessStrategy; // Synthetic properties are ones which are not part of the actual java model. // They're properties used for bookkeeping by Hibernate private boolean synthetic; @@ -323,6 +325,14 @@ public class PropertyAuditingData { this.propertyType = propertyType; } + public PropertyAccessStrategy getPropertyAccessStrategy() { + return propertyAccessStrategy; + } + + public void setPropertyAccessStrategy(PropertyAccessStrategy propertyAccessStrategy) { + this.propertyAccessStrategy = propertyAccessStrategy; + } + public Type getVirtualPropertyType() { return virtualPropertyType; } @@ -349,7 +359,8 @@ public class PropertyAuditingData { modifiedFlagName, synthetic, propertyType, - virtualPropertyType.getReturnedClass() + virtualPropertyType.getReturnedClass(), + propertyAccessStrategy ); } else if ( propertyType != null ) { @@ -360,7 +371,8 @@ public class PropertyAuditingData { usingModifiedFlag, modifiedFlagName, synthetic, - propertyType + propertyType, + propertyAccessStrategy ); } return new PropertyData( @@ -370,7 +382,8 @@ public class PropertyAuditingData { usingModifiedFlag, modifiedFlagName, synthetic, - null + null, + propertyAccessStrategy ); } } diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/PropertyData.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/PropertyData.java index 772ea2d6c2..8559d11c28 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/PropertyData.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/PropertyData.java @@ -8,6 +8,7 @@ package org.hibernate.envers.internal.entities; import java.util.Objects; +import org.hibernate.property.access.spi.PropertyAccessStrategy; import org.hibernate.type.Type; /** @@ -30,6 +31,7 @@ public class PropertyData { private boolean synthetic; private Type propertyType; private Class virtualReturnClass; + private PropertyAccessStrategy propertyAccessStrategy; /** * Copies the given property data, except the name. @@ -91,8 +93,9 @@ public class PropertyData { boolean usingModifiedFlag, String modifiedFlagName, boolean synthetic, - Type propertyType) { - this( name, beanName, accessType, usingModifiedFlag, modifiedFlagName, synthetic, propertyType, null ); + Type propertyType, + PropertyAccessStrategy propertyAccessStrategy) { + this( name, beanName, accessType, usingModifiedFlag, modifiedFlagName, synthetic, propertyType, null, propertyAccessStrategy ); } public PropertyData( @@ -103,10 +106,12 @@ public class PropertyData { String modifiedFlagName, boolean synthetic, Type propertyType, - Class virtualReturnClass) { + Class virtualReturnClass, + PropertyAccessStrategy propertyAccessStrategy) { this( name, beanName, accessType, usingModifiedFlag, modifiedFlagName, synthetic ); this.propertyType = propertyType; this.virtualReturnClass = virtualReturnClass; + this.propertyAccessStrategy = propertyAccessStrategy; } public String getName() { @@ -141,6 +146,10 @@ public class PropertyData { return virtualReturnClass; } + public PropertyAccessStrategy getPropertyAccessStrategy() { + return propertyAccessStrategy; + } + @Override public boolean equals(Object o) { if ( this == o ) { diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/ComponentPropertyMapper.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/ComponentPropertyMapper.java index 8cea6aa8f0..57751b8bff 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/ComponentPropertyMapper.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/ComponentPropertyMapper.java @@ -19,6 +19,7 @@ import org.hibernate.envers.internal.entities.PropertyData; import org.hibernate.envers.internal.reader.AuditReaderImplementor; import org.hibernate.envers.internal.tools.ReflectionTools; import org.hibernate.internal.util.ReflectHelper; +import org.hibernate.metamodel.spi.EmbeddableInstantiator; import org.hibernate.property.access.spi.Setter; /** @@ -31,9 +32,14 @@ public class ComponentPropertyMapper extends AbstractPropertyMapper implements C private final PropertyData propertyData; private final MultiPropertyMapper delegate; private final Class componentClass; + private final EmbeddableInstantiator embeddableInstantiator; - public ComponentPropertyMapper(PropertyData propertyData, Class componentClass) { + public ComponentPropertyMapper( + PropertyData propertyData, + Class componentClass, + EmbeddableInstantiator instantiator) { this.propertyData = propertyData; + this.embeddableInstantiator = instantiator; //if class is a map it means that this is dynamic component if ( Map.class.isAssignableFrom( componentClass ) ) { this.delegate = new MultiDynamicComponentMapper( propertyData ); @@ -51,8 +57,11 @@ public class ComponentPropertyMapper extends AbstractPropertyMapper implements C } @Override - public CompositeMapperBuilder addComponent(PropertyData propertyData, Class componentClass) { - return delegate.addComponent( propertyData, componentClass ); + public CompositeMapperBuilder addComponent( + PropertyData propertyData, + Class componentClass, + EmbeddableInstantiator instantiator) { + return delegate.addComponent( propertyData, componentClass, instantiator ); } @Override @@ -118,9 +127,9 @@ public class ComponentPropertyMapper extends AbstractPropertyMapper implements C doPrivileged( () -> { try { - final Object subObj = ReflectHelper.getDefaultConstructor( componentClass ).newInstance(); if ( isDynamicComponentMap() ) { + final Object subObj = ReflectHelper.getDefaultConstructor( componentClass ).newInstance(); ( (Map) obj ).put( propertyData.getBeanName(), subObj ); delegate.mapToEntityFromMap( enversService, subObj, data, primaryKey, versionsReader, revision ); } @@ -136,9 +145,39 @@ public class ComponentPropertyMapper extends AbstractPropertyMapper implements C setter.set( obj, null ); } else { + final Object subObj; + if ( embeddableInstantiator != null ) { + final Object[] values = new Object[delegate.properties.size()]; + int i = 0; + for ( Map.Entry entry : delegate.properties.entrySet() ) { + values[i] = entry.getValue().mapToEntityFromMap( + enversService, + data, + primaryKey, + versionsReader, + revision + ); + i++; + } + subObj = embeddableInstantiator.instantiate( + () -> values, + versionsReader.getSessionImplementor() + .getSessionFactory() + ); + } + else { + subObj = ReflectHelper.getDefaultConstructor( componentClass ).newInstance(); + delegate.mapToEntityFromMap( + enversService, + subObj, + data, + primaryKey, + versionsReader, + revision + ); + } // set the component setter.set( obj, subObj ); - delegate.mapToEntityFromMap( enversService, subObj, data, primaryKey, versionsReader, revision ); } } } @@ -150,6 +189,72 @@ public class ComponentPropertyMapper extends AbstractPropertyMapper implements C } ); } + @Override + public Object mapToEntityFromMap( + final EnversService enversService, + final Map data, + final Object primaryKey, + final AuditReaderImplementor versionsReader, + final Number revision) { + if ( data == null || propertyData.getBeanName() == null ) { + // If properties are not encapsulated in a component but placed directly in a class + // (e.g. by applying tag). + return null; + } + + return doPrivileged( () -> { + try { + final Object subObj; + if ( isDynamicComponentMap() ) { + subObj = ReflectHelper.getDefaultConstructor( componentClass ).newInstance(); + delegate.mapToEntityFromMap( enversService, subObj, data, primaryKey, versionsReader, revision ); + } + else { + if ( isAllPropertiesNull( data ) ) { + // single property, but default value need not be null, so we'll set it to null anyway + subObj = null; + } + else { + if ( embeddableInstantiator != null ) { + final Object[] values = new Object[delegate.properties.size()]; + int i = 0; + for ( Map.Entry entry : delegate.properties.entrySet() ) { + values[i] = entry.getValue().mapToEntityFromMap( + enversService, + data, + primaryKey, + versionsReader, + revision + ); + i++; + } + subObj = embeddableInstantiator.instantiate( + () -> values, + versionsReader.getSessionImplementor() + .getSessionFactory() + ); + } + else { + subObj = ReflectHelper.getDefaultConstructor( componentClass ).newInstance(); + delegate.mapToEntityFromMap( + enversService, + subObj, + data, + primaryKey, + versionsReader, + revision + ); + } + } + } + return subObj; + } + catch ( Exception e ) { + throw new AuditException( e ); + } + } ); + } + private boolean isAllPropertiesNull(Map data) { for ( Map.Entry property : delegate.getProperties().entrySet() ) { final Object value = data.get( property.getKey().getName() ); diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/CompositeMapperBuilder.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/CompositeMapperBuilder.java index 34a3ef9e2f..047dcf02f3 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/CompositeMapperBuilder.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/CompositeMapperBuilder.java @@ -9,12 +9,15 @@ package org.hibernate.envers.internal.entities.mapper; import java.util.Map; import org.hibernate.envers.internal.entities.PropertyData; +import org.hibernate.metamodel.spi.EmbeddableInstantiator; /** * @author Adam Warski (adam at warski dot org) */ public interface CompositeMapperBuilder extends SimpleMapperBuilder { - CompositeMapperBuilder addComponent(PropertyData propertyData, Class componentClass); + CompositeMapperBuilder addComponent( + PropertyData propertyData, + Class componentClass, EmbeddableInstantiator instantiator); void addComposite(PropertyData propertyData, PropertyMapper propertyMapper); diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/MultiDynamicComponentMapper.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/MultiDynamicComponentMapper.java index 939c71c525..999d27961f 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/MultiDynamicComponentMapper.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/MultiDynamicComponentMapper.java @@ -122,4 +122,14 @@ public class MultiDynamicComponentMapper extends MultiPropertyMapper { mapper.mapToEntityFromMap( enversService, obj, data, primaryKey, versionsReader, revision ); } } + + @Override + public Object mapToEntityFromMap( + EnversService enversService, + Map data, + Object primaryKey, + AuditReaderImplementor versionsReader, + Number revision) { + return null; + } } diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/MultiPropertyMapper.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/MultiPropertyMapper.java index aada560f3f..eb0c977fd3 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/MultiPropertyMapper.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/MultiPropertyMapper.java @@ -19,6 +19,7 @@ import org.hibernate.envers.internal.tools.MappingTools; import org.hibernate.envers.internal.tools.ReflectionTools; import org.hibernate.envers.internal.tools.Tools; import org.hibernate.envers.tools.Pair; +import org.hibernate.metamodel.spi.EmbeddableInstantiator; import org.hibernate.property.access.spi.Getter; /** @@ -45,7 +46,9 @@ public class MultiPropertyMapper extends AbstractPropertyMapper implements Exten } @Override - public CompositeMapperBuilder addComponent(PropertyData propertyData, Class componentClass) { + public CompositeMapperBuilder addComponent( + PropertyData propertyData, + Class componentClass, EmbeddableInstantiator instantiator) { if ( properties.get( propertyData ) != null ) { // This is needed for second pass to work properly in the components mapper return (CompositeMapperBuilder) properties.get( propertyData ); @@ -53,7 +56,8 @@ public class MultiPropertyMapper extends AbstractPropertyMapper implements Exten final ComponentPropertyMapper componentMapperBuilder = new ComponentPropertyMapper( propertyData, - componentClass + componentClass, + instantiator ); addComposite( propertyData, componentMapperBuilder ); @@ -198,6 +202,16 @@ public class MultiPropertyMapper extends AbstractPropertyMapper implements Exten } } + @Override + public Object mapToEntityFromMap( + EnversService enversService, + Map data, + Object primaryKey, + AuditReaderImplementor versionsReader, + Number revision) { + return null; + } + private Pair getMapperAndDelegatePropName(String referencingPropertyName) { // Name of the property, to which we will delegate the mapping. String delegatePropertyName; diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/PropertyMapper.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/PropertyMapper.java index 69879979b0..e995019dc5 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/PropertyMapper.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/PropertyMapper.java @@ -51,6 +51,13 @@ public interface PropertyMapper extends ModifiedFlagMapperSupport, DynamicCompon AuditReaderImplementor versionsReader, Number revision); + Object mapToEntityFromMap( + EnversService enversService, + Map data, + Object primaryKey, + AuditReaderImplementor versionsReader, + Number revision); + /** * Maps collection changes. * diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/SinglePropertyMapper.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/SinglePropertyMapper.java index 81852628d6..98d4d6093e 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/SinglePropertyMapper.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/SinglePropertyMapper.java @@ -120,6 +120,21 @@ public class SinglePropertyMapper extends AbstractPropertyMapper implements Simp } } + @Override + public Object mapToEntityFromMap( + final EnversService enversService, + final Map data, + Object primaryKey, + AuditReaderImplementor versionsReader, + Number revision) { + // synthetic properties are not part of the entity model; therefore they should be ignored. + if ( data == null || propertyData.isSynthetic() ) { + return null; + } + + return data.get( propertyData.getName() ); + } + private boolean isPrimitive(Setter setter, PropertyData propertyData, Class cls) { if ( cls == null ) { throw new HibernateException( "No field found for property: " + propertyData.getName() ); diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/SubclassPropertyMapper.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/SubclassPropertyMapper.java index f5a2f97d50..4af1c6563c 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/SubclassPropertyMapper.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/SubclassPropertyMapper.java @@ -16,6 +16,7 @@ import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.envers.boot.internal.EnversService; import org.hibernate.envers.internal.entities.PropertyData; import org.hibernate.envers.internal.reader.AuditReaderImplementor; +import org.hibernate.metamodel.spi.EmbeddableInstantiator; /** * A mapper which maps from a parent mapper and a "main" one, but adds only to the "main". The "main" mapper @@ -87,6 +88,16 @@ public class SubclassPropertyMapper extends AbstractPropertyMapper implements Ex main.mapToEntityFromMap( enversService, obj, data, primaryKey, versionsReader, revision ); } + @Override + public Object mapToEntityFromMap( + EnversService enversService, + Map data, + Object primaryKey, + AuditReaderImplementor versionsReader, + Number revision) { + return null; + } + @Override public List mapCollectionChanges( SessionImplementor session, String referencingPropertyName, @@ -120,8 +131,10 @@ public class SubclassPropertyMapper extends AbstractPropertyMapper implements Ex } @Override - public CompositeMapperBuilder addComponent(PropertyData propertyData, Class componentClass) { - return main.addComponent( propertyData, componentClass ); + public CompositeMapperBuilder addComponent( + PropertyData propertyData, + Class componentClass, EmbeddableInstantiator instantiator) { + return main.addComponent( propertyData, componentClass, instantiator ); } @Override diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/AbstractCollectionMapper.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/AbstractCollectionMapper.java index f1e1975d77..c74ce3d346 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/AbstractCollectionMapper.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/AbstractCollectionMapper.java @@ -276,6 +276,25 @@ public abstract class AbstractCollectionMapper extends AbstractPropertyMapper final Object primaryKey, final AuditReaderImplementor versionsReader, final Number revision) { + final Object collectionProxy = mapToEntityFromMap( enversService, data, primaryKey, versionsReader, revision ); + final PropertyData collectionPropertyData = commonCollectionMapperData.getCollectionReferencingPropertyData(); + + if ( isDynamicComponentMap() ) { + final Map map = (Map) obj; + map.put( collectionPropertyData.getBeanName(), collectionProxy ); + } + else { + setValueOnObject( collectionPropertyData, obj, collectionProxy, enversService.getServiceRegistry() ); + } + } + + @Override + public Object mapToEntityFromMap( + EnversService enversService, + Map data, + Object primaryKey, + AuditReaderImplementor versionsReader, + Number revision) { final String revisionTypePropertyName = enversService.getConfig().getRevisionTypePropertyName(); // construct the collection proxy @@ -294,16 +313,7 @@ public abstract class AbstractCollectionMapper extends AbstractPropertyMapper catch ( Exception e ) { throw new AuditException( "Failed to construct collection proxy", e ); } - - final PropertyData collectionPropertyData = commonCollectionMapperData.getCollectionReferencingPropertyData(); - - if ( isDynamicComponentMap() ) { - final Map map = (Map) obj; - map.put( collectionPropertyData.getBeanName(), collectionProxy ); - } - else { - setValueOnObject( collectionPropertyData, obj, collectionProxy, enversService.getServiceRegistry() ); - } + return collectionProxy; } /** diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/AbstractOneToOneMapper.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/AbstractOneToOneMapper.java index 72dadebda3..2c7916e722 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/AbstractOneToOneMapper.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/AbstractOneToOneMapper.java @@ -45,6 +45,17 @@ public abstract class AbstractOneToOneMapper extends AbstractToOneMapper { Object primaryKey, AuditReaderImplementor versionsReader, Number revision) { + Object value = nullSafeMapToEntityFromMap( enversService, data, primaryKey, versionsReader, revision ); + setPropertyValue( obj, value ); + } + + @Override + public Object nullSafeMapToEntityFromMap( + EnversService enversService, + Map data, + Object primaryKey, + AuditReaderImplementor versionsReader, + Number revision) { final EntityInfo referencedEntity = getEntityInfo( enversService, referencedEntityName ); Object value; @@ -60,8 +71,7 @@ public abstract class AbstractOneToOneMapper extends AbstractToOneMapper { "." + getPropertyData().getBeanName() + ".", e ); } - - setPropertyValue( obj, value ); + return value; } /** diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/AbstractToOneMapper.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/AbstractToOneMapper.java index fa234133d4..a83ba9280f 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/AbstractToOneMapper.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/AbstractToOneMapper.java @@ -58,6 +58,16 @@ public abstract class AbstractToOneMapper extends AbstractPropertyMapper { } } + @Override + public Object mapToEntityFromMap( + EnversService enversService, + Map data, + Object primaryKey, + AuditReaderImplementor versionsReader, + Number revision) { + return nullSafeMapToEntityFromMap( enversService, data, primaryKey, versionsReader, revision ); + } + @Override public List mapCollectionChanges( SessionImplementor session, @@ -115,6 +125,13 @@ public abstract class AbstractToOneMapper extends AbstractPropertyMapper { AuditReaderImplementor versionsReader, Number revision); + public abstract Object nullSafeMapToEntityFromMap( + EnversService enversService, + Map data, + Object primaryKey, + AuditReaderImplementor versionsReader, + Number revision); + /** * Simple descriptor of an entity. */ diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/ToOneIdMapper.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/ToOneIdMapper.java index 4143f67a5b..b969013e0a 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/ToOneIdMapper.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/ToOneIdMapper.java @@ -149,6 +149,17 @@ public class ToOneIdMapper extends AbstractToOneMapper { Object primaryKey, AuditReaderImplementor versionsReader, Number revision) { + Object value = nullSafeMapToEntityFromMap( enversService, data, primaryKey, versionsReader, revision ); + setPropertyValue( obj, value ); + } + + @Override + public Object nullSafeMapToEntityFromMap( + EnversService enversService, + Map data, + Object primaryKey, + AuditReaderImplementor versionsReader, + Number revision) { final Object entityId = delegate.mapToIdFromMap( data ); Object value = null; if ( entityId != null ) { @@ -183,8 +194,7 @@ public class ToOneIdMapper extends AbstractToOneMapper { } } } - - setPropertyValue( obj, value ); + return value; } public void addMiddleEqualToQuery( diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/component/MiddleEmbeddableComponentMapper.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/component/MiddleEmbeddableComponentMapper.java index 8365abcbb6..a24d6ee288 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/component/MiddleEmbeddableComponentMapper.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/component/MiddleEmbeddableComponentMapper.java @@ -17,6 +17,7 @@ import org.hibernate.envers.internal.entities.mapper.MultiPropertyMapper; import org.hibernate.envers.internal.entities.mapper.PropertyMapper; import org.hibernate.envers.internal.entities.mapper.relation.ToOneIdMapper; import org.hibernate.envers.internal.tools.query.Parameters; +import org.hibernate.metamodel.spi.EmbeddableInstantiator; /** * @author Kristoffer Lundberg (kristoffer at cambio dot se) @@ -125,8 +126,10 @@ public class MiddleEmbeddableComponentMapper extends AbstractMiddleComponentMapp } @Override - public CompositeMapperBuilder addComponent(PropertyData propertyData, Class componentClass) { - return delegate.addComponent( propertyData, componentClass ); + public CompositeMapperBuilder addComponent( + PropertyData propertyData, + Class componentClass, EmbeddableInstantiator instantiator) { + return delegate.addComponent( propertyData, componentClass, instantiator ); } @Override diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/tools/ReflectionTools.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/tools/ReflectionTools.java index 67cdf47644..0c67f2faec 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/internal/tools/ReflectionTools.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/tools/ReflectionTools.java @@ -47,7 +47,23 @@ public abstract class ReflectionTools { } public static Getter getGetter(Class cls, PropertyData propertyData, ServiceRegistry serviceRegistry) { - return getGetter( cls, propertyData.getBeanName(), propertyData.getAccessType(), serviceRegistry ); + if ( propertyData.getPropertyAccessStrategy() == null ) { + return getGetter( cls, propertyData.getBeanName(), propertyData.getAccessType(), serviceRegistry ); + } + else { + final String propertyName = propertyData.getName(); + final Pair key = Pair.make( cls, propertyName ); + Getter value = GETTER_CACHE.get( key ); + if ( value == null ) { + value = propertyData.getPropertyAccessStrategy().buildPropertyAccess( + cls, + propertyData.getBeanName(), false + ).getGetter(); + // It's ok if two getters are generated concurrently + GETTER_CACHE.put( key, value ); + } + return value; + } } public static Getter getGetter(Class cls, String propertyName, String accessorType, ServiceRegistry serviceRegistry) { diff --git a/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/customtype/ObjectUserType.java b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/customtype/ObjectUserType.java index 8d10bca420..ed53469deb 100644 --- a/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/customtype/ObjectUserType.java +++ b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/customtype/ObjectUserType.java @@ -6,23 +6,14 @@ */ package org.hibernate.envers.test.integration.customtype; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.io.OutputStream; import java.io.Serializable; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Types; +import java.util.function.Supplier; import org.hibernate.HibernateException; -import org.hibernate.NotYetImplementedFor6Exception; -import org.hibernate.engine.spi.SharedSessionContractImplementor; -import org.hibernate.usertype.UserType; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.usertype.CompositeUserType; + +import jakarta.persistence.Lob; /** * Custom type used to persist binary representation of Java object in the database. @@ -31,16 +22,31 @@ import org.hibernate.usertype.UserType; * * @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com) */ -public class ObjectUserType implements UserType { - private static final int[] TYPES = new int[] {Types.VARCHAR, Types.BLOB}; +public class ObjectUserType implements CompositeUserType { @Override - public int[] sqlTypes() { - return TYPES; + public Object getPropertyValue(Object component, int property) throws HibernateException { + switch ( property ) { + case 0: + return component; + case 1: + return component.getClass().getName(); + } + return null; } @Override - public Class returnedClass() { + public Object instantiate(Supplier values, SessionFactoryImplementor sessionFactory) { + return values.get()[0]; + } + + @Override + public Class embeddable() { + return TaggedObject.class; + } + + @Override + public Class returnedClass() { return Object.class; } @@ -60,77 +66,6 @@ public class ObjectUserType implements UserType { return x.hashCode(); } - @Override - public Object nullSafeGet(ResultSet rs, int position, SharedSessionContractImplementor session, Object owner) throws SQLException { - throw new NotYetImplementedFor6Exception( - "See https://github.com/hibernate/hibernate-orm/discussions/3960" - ); - } - - @Override - public void nullSafeSet(PreparedStatement st, Object value, int index, SharedSessionContractImplementor session) - throws HibernateException, SQLException { - if ( value == null ) { - st.setNull( index, TYPES[0] ); - st.setNull( index + 1, TYPES[1] ); - } - else { - st.setString( index, value.getClass().getName() ); - st.setBinaryStream( index + 1, convertObjectToInputStream( value ) ); - } - } - - private InputStream convertObjectToInputStream(Object value) { - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - ObjectOutputStream objectOutputStream = null; - try { - objectOutputStream = new ObjectOutputStream( byteArrayOutputStream ); - objectOutputStream.writeObject( value ); - objectOutputStream.flush(); - return new ByteArrayInputStream( byteArrayOutputStream.toByteArray() ); - } - catch (IOException e) { - throw new RuntimeException( e ); - } - finally { - closeQuietly( objectOutputStream ); - } - } - - private Object convertInputStreamToObject(InputStream inputStream) { - ObjectInputStream objectInputStream = null; - try { - objectInputStream = new ObjectInputStream( inputStream ); - return objectInputStream.readObject(); - } - catch (Exception e) { - throw new RuntimeException( e ); - } - finally { - closeQuietly( objectInputStream ); - } - } - - private void closeQuietly(OutputStream stream) { - if ( stream != null ) { - try { - stream.close(); - } - catch (IOException e) { - } - } - } - - private void closeQuietly(InputStream stream) { - if ( stream != null ) { - try { - stream.close(); - } - catch (IOException e) { - } - } - } - @Override public Object deepCopy(Object value) throws HibernateException { return value; // Persisting only immutable types. @@ -155,4 +90,10 @@ public class ObjectUserType implements UserType { public Object replace(Object original, Object target, Object owner) throws HibernateException { return original; } + + public static class TaggedObject { + String type; + @Lob + Serializable object; + } } diff --git a/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/customtype/ObjectUserTypeEntity.java b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/customtype/ObjectUserTypeEntity.java index fe9143d176..b5b0a769d6 100644 --- a/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/customtype/ObjectUserTypeEntity.java +++ b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/customtype/ObjectUserTypeEntity.java @@ -7,14 +7,16 @@ package org.hibernate.envers.test.integration.customtype; import java.io.Serializable; + +import org.hibernate.annotations.CompositeType; +import org.hibernate.envers.Audited; + +import jakarta.persistence.AttributeOverride; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; -import org.hibernate.annotations.Columns; -import org.hibernate.envers.Audited; - /** * Entity encapsulating {@link Object} property which concrete type may change during subsequent updates. * @@ -29,8 +31,10 @@ public class ObjectUserTypeEntity implements Serializable { private String buildInType; -// @Type(type = "org.hibernate.envers.test.integration.customtype.ObjectUserType") - @Columns(columns = {@Column(name = "OBJ_TYPE"), @Column(name = "OBJ_VALUE")}) + @Audited + @CompositeType(ObjectUserType.class) + @AttributeOverride( name = "type", column = @Column(name = "OBJ_TYPE")) + @AttributeOverride( name = "object", column = @Column(name = "OBJ_VALUE")) private Object userType; public ObjectUserTypeEntity() { diff --git a/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/customtype/ObjectUserTypeTest.java b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/customtype/ObjectUserTypeTest.java index f6e2648752..bb138dec0d 100644 --- a/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/customtype/ObjectUserTypeTest.java +++ b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/customtype/ObjectUserTypeTest.java @@ -10,12 +10,10 @@ import java.util.Arrays; import java.util.Map; import jakarta.persistence.EntityManager; -import org.hibernate.dialect.Oracle8iDialect; import org.hibernate.envers.configuration.EnversSettings; import org.hibernate.orm.test.envers.BaseEnversJPAFunctionalTestCase; import org.hibernate.orm.test.envers.Priority; -import org.hibernate.testing.RequiresDialect; import org.hibernate.testing.TestForIssue; import org.junit.Assert; import org.junit.Test; @@ -24,7 +22,6 @@ import org.junit.Test; * @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com) */ @TestForIssue(jiraKey = "HHH-7870") -@RequiresDialect(Oracle8iDialect.class) public class ObjectUserTypeTest extends BaseEnversJPAFunctionalTestCase { private int id; diff --git a/migration-guide.adoc b/migration-guide.adoc index 723ecfff2a..d13c3a72bf 100644 --- a/migration-guide.adoc +++ b/migration-guide.adoc @@ -198,6 +198,32 @@ identifiers cannot use constructor injection. See https://docs.jboss.org/hibernate/orm/6.0/userguide/html_single/Hibernate_User_Guide.html#embeddable-instantiator for details. +==== CompositeUserType changes + +The `CompositeUserType` interface was re-implemented to be able to model user types as proper embeddable types. +A major difference to 5.x is the introduction of an "embeddable projection" that is used to determine the mapping structure. + +Previously, a `CompositeUserType` had to provide property names and types through dedicated accessor methods, +but this was complicated for non-basic mappings and required quite some knowledge about Hibernate internals. +With 6.0 these methods are replaced with a method that returns an "embeddable projection" class. +The class is like a regular `@Embeddable` class and is used to determine the mapping structure for the `CompositeUserType`. + +Component values of a user type object are accessed by property index. The property index is 0-based and can be determined +by sorting the persistent attribute names lexicographically ascending and using the 0-based position as property index. + +For example, the following component: + +```java +public class MonetaryAmountEmbeddable { + BigDecimal value; + Currency currency; +} +``` + +will assign property index 0 to `currency` and index 1 to `value`. + +Note that it is not possible anymore to use `@Columns` to specify the names of columns of a composite user type mapping. +Since a `CompositeUserType` now constructs a proper component, it is necessary to use the `@AttributeOverride` annotation. === Plural attributes @@ -757,4 +783,54 @@ Prior to Hibernate 6.0, lazy associations that used `fetch="join"` or `@Fetch(Fe when loaded by-id i.e. through `Session#get`/`EntityManager#find`, even though for queries the association was treated as lazy. Starting with Hibernate 6.0, the laziness of such associations is properly respected, regardless of the fetch mechanism. -Backwards compatibility can be achieved by specifying `lazy="false"` or `@ManyToOne(fetch = EAGER)`/`@OneToOne(fetch = EAGER)`/`@OneToMany(fetch = EAGER)`/`@ManyToMany(fetch = EAGER)`. \ No newline at end of file +Backwards compatibility can be achieved by specifying `lazy="false"` or `@ManyToOne(fetch = EAGER)`/`@OneToOne(fetch = EAGER)`/`@OneToMany(fetch = EAGER)`/`@ManyToMany(fetch = EAGER)`. + +== hbm.xml behavior change + +As of Hibernate 6.0, a `` will cause a fetch of an association, rather than adding a selection item. +Consider the following example: + +```xml + + + + + + + + + + + ... + +``` + +Prior to 6.0, a query would return a list of tuples [`Organization`, `Employee`], +but now this will return a list of `Organization` with an initialized `employments` collection. + +== hbm.xml multiple now disallowed + +In 6.0 the support for basic property mappings with multiple columns was removed. The only use case for that was when a +`CompositeUserType` was in use, which was reworked to now work on top of components. + +Uses like: + +```xml + + + + +``` + +have to be migrated to proper components: + +```xml + + + + + + +``` + +The component class attribute now supports interpreting a `CompositeUserType` class properly.