diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/MetadataContext.java b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/MetadataContext.java index 0264f7543d..490b65f3c9 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/MetadataContext.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/MetadataContext.java @@ -288,7 +288,12 @@ public class MetadataContext { ); if ( attribute != null ) { ( (AttributeContainer) jpaMapping ).getInFlightAccess().addAttribute( attribute ); + if ( property.isNaturalIdentifier() ) { + ( ( AttributeContainer) jpaMapping ).getInFlightAccess() + .applyNaturalIdAttribute( attribute ); + } } + } ( (AttributeContainer) jpaMapping ).getInFlightAccess().finishUp(); @@ -313,6 +318,7 @@ public class MetadataContext { applyIdMetadata( safeMapping, jpaType ); applyVersionAttribute( safeMapping, jpaType ); +// applyNaturalIdAttribute( safeMapping, jpaType ); Iterator properties = safeMapping.getDeclaredPropertyIterator(); while ( properties.hasNext() ) { @@ -324,6 +330,10 @@ public class MetadataContext { final PersistentAttribute attribute = attributeFactory.buildAttribute( jpaType, property ); if ( attribute != null ) { ( (AttributeContainer) jpaType ).getInFlightAccess().addAttribute( attribute ); + if ( property.isNaturalIdentifier() ) { + ( ( AttributeContainer) jpaType ).getInFlightAccess() + .applyNaturalIdAttribute( attribute ); + } } } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/AbstractIdentifiableType.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/AbstractIdentifiableType.java index 200ac31d15..9482b53af7 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/AbstractIdentifiableType.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/AbstractIdentifiableType.java @@ -7,8 +7,10 @@ package org.hibernate.metamodel.model.domain; import java.io.Serializable; +import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.function.Consumer; import jakarta.persistence.metamodel.Bindable; @@ -53,6 +55,7 @@ public abstract class AbstractIdentifiableType private final boolean isVersioned; private SingularPersistentAttribute versionAttribute; + private List> naturalIdAttributes; public AbstractIdentifiableType( String typeName, @@ -250,7 +253,7 @@ public abstract class AbstractIdentifiableType return null; } - SingularPersistentAttribute version = findVersionAttribute(); + SingularPersistentAttribute version = findVersionAttribute(); if ( version != null ) { checkType( version, javaType ); } @@ -258,14 +261,26 @@ public abstract class AbstractIdentifiableType } @Override - @SuppressWarnings("unchecked") - public SingularPersistentAttribute findVersionAttribute() { + public SingularPersistentAttribute findVersionAttribute() { if ( versionAttribute != null ) { return versionAttribute; } if ( getSuperType() != null ) { - return (SingularPersistentAttribute) getSuperType().findVersionAttribute(); + return getSuperType().findVersionAttribute(); + } + + return null; + } + + @Override + public List> findNaturalIdAttributes() { + if ( naturalIdAttributes != null ) { + return naturalIdAttributes; + } + + if ( getSuperType() != null ) { + return getSuperType().findNaturalIdAttributes(); } return null; @@ -362,6 +377,14 @@ public abstract class AbstractIdentifiableType managedTypeAccess.addAttribute( versionAttribute ); } + @Override + public void applyNaturalIdAttribute(PersistentAttribute naturalIdAttribute) { + if ( AbstractIdentifiableType.this.naturalIdAttributes == null ) { + AbstractIdentifiableType.this.naturalIdAttributes = new ArrayList<>(); + } + AbstractIdentifiableType.this.naturalIdAttributes.add( naturalIdAttribute ); + } + @Override public void addAttribute(PersistentAttribute attribute) { managedTypeAccess.addAttribute( attribute ); diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/IdentifiableDomainType.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/IdentifiableDomainType.java index bb755c8c27..2905f8186a 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/IdentifiableDomainType.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/IdentifiableDomainType.java @@ -6,6 +6,7 @@ */ package org.hibernate.metamodel.model.domain; +import java.util.List; import java.util.Set; import java.util.function.Consumer; import jakarta.persistence.metamodel.IdentifiableType; @@ -49,4 +50,6 @@ public interface IdentifiableDomainType extends ManagedDomainType, Identif void visitIdClassAttributes(Consumer> action); SingularPersistentAttribute findVersionAttribute(); + + List> findNaturalIdAttributes(); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AttributeContainer.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AttributeContainer.java index 260fd76fe3..ad2b82b3ed 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AttributeContainer.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AttributeContainer.java @@ -60,6 +60,11 @@ public interface AttributeContainer { ); } + default void applyNaturalIdAttribute(PersistentAttribute versionAttribute) { + throw new UnsupportedMappingException( + "AttributeContainer [" + getClass().getName() + "] does not support natural ids" + ); + } /** * Called when configuration of the type is complete diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java index 3eadd6634a..2f9292b53d 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java @@ -33,6 +33,7 @@ import java.util.Map; import java.util.Set; import java.util.regex.Pattern; +import jakarta.persistence.metamodel.SingularAttribute; import org.hibernate.NotYetImplementedFor6Exception; import org.hibernate.QueryException; import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; @@ -52,6 +53,7 @@ import org.hibernate.metamodel.model.domain.DomainType; import org.hibernate.metamodel.model.domain.EntityDomainType; import org.hibernate.metamodel.model.domain.IdentifiableDomainType; import org.hibernate.metamodel.model.domain.ManagedDomainType; +import org.hibernate.metamodel.model.domain.PersistentAttribute; import org.hibernate.metamodel.model.domain.PluralPersistentAttribute; import org.hibernate.metamodel.model.domain.SingularPersistentAttribute; import org.hibernate.query.BinaryArithmeticOperator; @@ -2175,7 +2177,33 @@ public class SemanticQueryBuilder extends HqlParserBaseVisitor implem @Override public SqmPath visitEntityNaturalIdReference(HqlParser.EntityNaturalIdReferenceContext ctx) { - throw new NotYetImplementedFor6Exception( "Support for HQL natural-id references not yet implemented" ); + final SqmPath sqmPath = consumeDomainPath( (HqlParser.PathContext) ctx.getChild( 2 ) ); + final DomainType sqmPathType = sqmPath.getReferencedPathSource().getSqmPathType(); + + if ( sqmPathType instanceof IdentifiableDomainType ) { + @SuppressWarnings("unchecked") + final IdentifiableDomainType identifiableType = (IdentifiableDomainType) sqmPathType; + final List> attributes = identifiableType.findNaturalIdAttributes(); + if ( attributes == null ) { + throw new SemanticException( + "`" + sqmPath.getNavigablePath().getFullPath() + "` resolved to an identifiable-type (`" + + identifiableType.getTypeName() + "`) which does not define a natural id" + ); + } + else if ( attributes.size() >1 ) { + throw new SemanticException( + "`" + sqmPath.getNavigablePath().getFullPath() + "` resolved to an identifiable-type (`" + + identifiableType.getTypeName() + "`) which defines multiple natural ids" + ); + } + + @SuppressWarnings("unchecked") + SingularAttribute naturalIdAttribute + = (SingularAttribute) attributes.get(0); + return sqmPath.get( naturalIdAttribute ); + } + + throw new SemanticException( "Path does not reference an identifiable-type : " + sqmPath.getNavigablePath().getFullPath() ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmPolymorphicRootDescriptor.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmPolymorphicRootDescriptor.java index 481ee9398c..172f5ede50 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmPolymorphicRootDescriptor.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmPolymorphicRootDescriptor.java @@ -467,6 +467,11 @@ public class SqmPolymorphicRootDescriptor implements EntityDomainType { throw new UnsupportedOperationException( ); } + @Override + public List> findNaturalIdAttributes() { + throw new UnsupportedOperationException( ); + } + @Override public boolean hasSingleIdAttribute() { throw new UnsupportedOperationException( ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/FunctionTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/FunctionTests.java index 6951cbfa31..67643667e1 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/FunctionTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/FunctionTests.java @@ -60,6 +60,20 @@ public class FunctionTests { ); } + @Test + public void testIdVersionFunctions(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createQuery("select id(w) from VersionedEntity w") + .list(); + session.createQuery("select version(w) from VersionedEntity w") + .list(); + session.createQuery("select naturalid(w) from VersionedEntity w") + .list(); + } + ); + } + @Test @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsCharCodeConversion.class) public void testAsciiChrFunctions(SessionFactoryScope scope) { diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/domain/gambit/GambitDomainModel.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/domain/gambit/GambitDomainModel.java index d436c16952..966a29bebf 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/domain/gambit/GambitDomainModel.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/domain/gambit/GambitDomainModel.java @@ -17,6 +17,7 @@ public class GambitDomainModel extends AbstractDomainModelDescriptor { public GambitDomainModel() { super( BasicEntity.class, + VersionedEntity.class, Component.class, EmbeddedIdEntity.class, EntityOfArrays.class, diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/domain/gambit/VersionedEntity.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/domain/gambit/VersionedEntity.java new file mode 100644 index 0000000000..b5c394fd1c --- /dev/null +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/domain/gambit/VersionedEntity.java @@ -0,0 +1,71 @@ +/* + * 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.testing.orm.domain.gambit; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Version; +import org.hibernate.annotations.NaturalId; + +import java.util.Objects; + +/** + * @author Chris Cranford + */ +@Entity +public class VersionedEntity { + @Id + private Integer id; + @Version + private Integer version; + @NaturalId + private String code; + private String data; + + public VersionedEntity() { + + } + + public VersionedEntity(Integer id, String data) { + this.id = id; + this.data = data; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + VersionedEntity that = (VersionedEntity) o; + return Objects.equals( id, that.id ) && + Objects.equals( data, that.data ); + } + + @Override + public int hashCode() { + return Objects.hash( id, data ); + } +}