From a8a177886e19ef6d64c8be54e0dc0776d88fc4af Mon Sep 17 00:00:00 2001 From: Jan-Willem Gmelig Meyling Date: Sat, 27 Jun 2020 20:14:22 +0200 Subject: [PATCH] HHH-14198 - Expose CompositeUserTypes through JPA Metamodel Composite User Types work like regular Composite Types (like Embeddable) in HQL. However, because they cannot be represented in the JPA metamodel, libraries like [GraphQL for JPA](https://github.com/jcrygier/graphql-jpa) or [Blaze-Persistence](https://persistence.blazebit.com/) cannot fully utilize them. In order to make the composite property names available to these libraries, it would be nice to optionally expose these attributes as embedded attributes. This pull request aims to make that change and makes it configurable through a custom setting. Composite User Types are a common solution for mapping composite interfaces. A common example is for example `Money` from the Java Money API (JSR-354), for which composite user types are implemented in [Jadira](http://jadira.sourceforge.net/usertype-userguide.html). I know Composite User Types are currently not consiered in Hibernate 6.x. See also [this](https://hibernate.zulipchat.com/#narrow/stream/132094-hibernate-orm-dev/topic/CompositeUserType) Zulip thread. I am not sure if Hibernate 6.x will even have multi column types, which I presume would be a requirement to even introduce Composite User types back at some point. Usually Embeddables are a much easier, suitable mechanism for composite user types. But Embeddables are not always a viable alternative, because Embeddables require the type to be subclassed (as an interface cannot be mapped, and the type may not solely comprise fields that can be mapped to a simple basic type). To deal with this exact problem, `MonetaryAmounts` are still mapped as composite user type. There also have been suggestions to the JPA Spec to consider `AttributeConverters` for Embeddables for pracitcally the same purpose (which I think is going to be a mess of an implementation). See: https://github.com/eclipse-ee4j/jpa-api/issues/105 Anyways, regardless of whether this gets integrated in 5.x, I don't expect it to be integrated in 6.x unless we also reintroduce Composite User Types. I am willing to contribute Composite User Types for 6.x if people see benefit in it and think it can be done in the first place. --- .../metamodel/internal/AttributeFactory.java | 44 +++++++++++++++++-- .../domain/internal/EmbeddableTypeImpl.java | 8 ++-- .../domain/spi/EmbeddedTypeDescriptor.java | 4 +- .../test/cut/CompositeUserTypeTest.java | 28 ++++++++++++ 4 files changed, 74 insertions(+), 10 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/AttributeFactory.java b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/AttributeFactory.java index 34fc6a6fb8..455b50305f 100755 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/AttributeFactory.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/AttributeFactory.java @@ -36,6 +36,7 @@ import org.hibernate.metamodel.model.domain.internal.MapMember; import org.hibernate.metamodel.model.domain.internal.MappedSuperclassTypeImpl; import org.hibernate.metamodel.model.domain.internal.PluralAttributeBuilder; import org.hibernate.metamodel.model.domain.internal.SingularAttributeImpl; +import org.hibernate.metamodel.model.domain.spi.ManagedTypeDescriptor.InFlightAccess; import org.hibernate.metamodel.model.domain.spi.PersistentAttributeDescriptor; import org.hibernate.metamodel.model.domain.spi.EmbeddedTypeDescriptor; import org.hibernate.metamodel.model.domain.spi.IdentifiableTypeDescriptor; @@ -46,6 +47,7 @@ import org.hibernate.property.access.internal.PropertyAccessMapImpl; import org.hibernate.property.access.spi.Getter; import org.hibernate.tuple.entity.EntityMetamodel; import org.hibernate.type.ComponentType; +import org.hibernate.type.CompositeType; import org.hibernate.type.EmbeddedComponentType; import org.hibernate.type.EntityType; @@ -95,11 +97,45 @@ public class AttributeFactory { return buildPluralAttribute( (PluralAttributeMetadata) attributeMetadata ); } final SingularAttributeMetadata singularAttributeMetadata = (SingularAttributeMetadata) attributeMetadata; - final SimpleTypeDescriptor metaModelType = determineSimpleType( singularAttributeMetadata.getValueContext() ); + SimpleTypeDescriptor metaModelType = determineSimpleType( singularAttributeMetadata.getValueContext() ); + Attribute.PersistentAttributeType jpaAttributeNature = attributeMetadata.getJpaAttributeNature(); + + if ( attributeContext.getPropertyMapping().getType().isComponentType() && jpaAttributeNature.equals( Attribute.PersistentAttributeType.BASIC ) ) { + CompositeType compositeType = (CompositeType) attributeContext.getPropertyMapping().getType(); + EmbeddableTypeImpl embeddableType = new EmbeddableTypeImpl<>( + attributeMetadata.getJavaType(), + ownerType, + compositeType, + context.getSessionFactory() + ); + context.registerEmbeddedableType(embeddableType); + + String[] propertyNames = compositeType.getPropertyNames(); + org.hibernate.type.Type[] subtypes = compositeType.getSubtypes(); + InFlightAccess inFlightAccess = embeddableType.getInFlightAccess(); + + for ( int i = 0; i < propertyNames.length; i++ ) { + SingularAttributeImpl nestedAttribute = new SingularAttributeImpl( + embeddableType, + propertyNames[i], + Attribute.PersistentAttributeType.BASIC, + new BasicTypeImpl(subtypes[i].getReturnedClass(), Type.PersistenceType.BASIC), + null, + false, + false, + property.isOptional() + ); + inFlightAccess.addAttribute(nestedAttribute); + } + + metaModelType = embeddableType; + jpaAttributeNature = Attribute.PersistentAttributeType.EMBEDDED; + } + return new SingularAttributeImpl( ownerType, attributeMetadata.getName(), - attributeMetadata.getJpaAttributeNature(), + jpaAttributeNature, metaModelType, attributeMetadata.getMember(), false, @@ -230,7 +266,7 @@ public class AttributeFactory { ); context.registerEmbeddedableType( embeddableType ); - final ManagedTypeDescriptor.InFlightAccess inFlightAccess = embeddableType.getInFlightAccess(); + final InFlightAccess inFlightAccess = embeddableType.getInFlightAccess(); final Iterator subProperties = component.getPropertyIterator(); while ( subProperties.hasNext() ) { final Property property = subProperties.next(); @@ -954,7 +990,7 @@ public class AttributeFactory { final EmbeddedTypeDescriptor embeddableType = (EmbeddedTypeDescriptor) attributeContext.getOwnerType(); final String attributeName = attributeContext.getPropertyMapping().getName(); - final Getter getter = embeddableType.getHibernateType() + final Getter getter = ( ( ComponentType ) embeddableType.getHibernateType() ) .getComponentTuplizer() .getGetter( embeddableType.getHibernateType().getPropertyIndex( attributeName ) ); return PropertyAccessMapImpl.GetterImpl.class.isInstance( getter ) diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/EmbeddableTypeImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/EmbeddableTypeImpl.java index 100be7a13e..95efae1fbd 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/EmbeddableTypeImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/EmbeddableTypeImpl.java @@ -13,7 +13,7 @@ import org.hibernate.graph.internal.SubGraphImpl; import org.hibernate.graph.spi.SubGraphImplementor; import org.hibernate.metamodel.model.domain.spi.EmbeddedTypeDescriptor; import org.hibernate.metamodel.model.domain.spi.ManagedTypeDescriptor; -import org.hibernate.type.ComponentType; +import org.hibernate.type.CompositeType; /** * Standard Hibernate implementation of JPA's {@link javax.persistence.metamodel.EmbeddableType} @@ -27,12 +27,12 @@ public class EmbeddableTypeImpl implements EmbeddedTypeDescriptor, Serializable { private final ManagedTypeDescriptor parent; - private final ComponentType hibernateType; + private final CompositeType hibernateType; public EmbeddableTypeImpl( Class javaType, ManagedTypeDescriptor parent, - ComponentType hibernateType, + CompositeType hibernateType, SessionFactoryImplementor sessionFactory) { super( javaType, null, null, sessionFactory ); this.parent = parent; @@ -48,7 +48,7 @@ public class EmbeddableTypeImpl return parent; } - public ComponentType getHibernateType() { + public CompositeType getHibernateType() { return hibernateType; } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/spi/EmbeddedTypeDescriptor.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/spi/EmbeddedTypeDescriptor.java index 3190d74358..de7af0c106 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/spi/EmbeddedTypeDescriptor.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/spi/EmbeddedTypeDescriptor.java @@ -9,7 +9,7 @@ package org.hibernate.metamodel.model.domain.spi; import javax.persistence.metamodel.EmbeddableType; import org.hibernate.metamodel.model.domain.EmbeddedDomainType; -import org.hibernate.type.ComponentType; +import org.hibernate.type.CompositeType; /** * Hibernate extension to the JPA {@link EmbeddableType} descriptor @@ -17,7 +17,7 @@ import org.hibernate.type.ComponentType; * @author Steve Ebersole */ public interface EmbeddedTypeDescriptor extends EmbeddedDomainType, ManagedTypeDescriptor { - ComponentType getHibernateType(); + CompositeType getHibernateType(); ManagedTypeDescriptor getParent(); } diff --git a/hibernate-core/src/test/java/org/hibernate/test/cut/CompositeUserTypeTest.java b/hibernate-core/src/test/java/org/hibernate/test/cut/CompositeUserTypeTest.java index 1f02f107b7..7d8d3678d1 100755 --- a/hibernate-core/src/test/java/org/hibernate/test/cut/CompositeUserTypeTest.java +++ b/hibernate-core/src/test/java/org/hibernate/test/cut/CompositeUserTypeTest.java @@ -6,18 +6,24 @@ */ package org.hibernate.test.cut; +import java.lang.reflect.Member; import java.math.BigDecimal; import java.util.Currency; import java.util.List; import org.hibernate.Query; import org.hibernate.Session; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.cfg.Configuration; import org.hibernate.criterion.Restrictions; import org.hibernate.dialect.DB2Dialect; import org.hibernate.dialect.HSQLDialect; import org.hibernate.dialect.SybaseASE15Dialect; import org.hibernate.hql.internal.ast.QuerySyntaxException; +import org.hibernate.metamodel.model.domain.spi.PersistentAttributeDescriptor; +import org.hibernate.metamodel.model.domain.spi.SingularPersistentAttribute; +import org.hibernate.metamodel.spi.MetamodelImplementor; import org.hibernate.testing.DialectChecks; import org.hibernate.testing.RequiresDialectFeature; import org.hibernate.testing.SkipForDialect; @@ -25,13 +31,20 @@ import org.hibernate.testing.TestForIssue; import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; import org.junit.Test; +import javax.persistence.metamodel.Attribute; +import javax.persistence.metamodel.ManagedType; +import javax.persistence.metamodel.SingularAttribute; + import static org.hibernate.testing.junit4.ExtraAssertions.assertTyping; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; /** * @author Gavin King */ public class CompositeUserTypeTest extends BaseCoreFunctionalTestCase { + @Override public String[] getMappings() { return new String[] { "cut/types.hbm.xml", "cut/Transaction.hbm.xml" }; @@ -67,6 +80,21 @@ public class CompositeUserTypeTest extends BaseCoreFunctionalTestCase { t.commit(); s.close(); } + + @Test + public void testMetamodel() { + MetamodelImplementor metamodel = sessionFactory().getMetamodel(); + PersistentAttributeDescriptor value = metamodel.managedType(Transaction.class).getAttribute("value"); + assertEquals(Attribute.PersistentAttributeType.EMBEDDED, value.getPersistentAttributeType()); + + SingularPersistentAttribute singularPersistentAttribute = (SingularPersistentAttribute) value; + ManagedType attribute = (ManagedType) singularPersistentAttribute.getType(); + SingularAttribute amount = attribute.getSingularAttribute("amount"); + assertNotNull(amount); + + Member javaMember = amount.getJavaMember(); + assertNull(javaMember); + } @Test @SkipForDialect ( value = { SybaseASE15Dialect.class }, jiraKey = "HHH-6788" )