diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/NaturalIdClass.java b/hibernate-core/src/main/java/org/hibernate/annotations/NaturalIdClass.java new file mode 100644 index 000000000000..083427755806 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/annotations/NaturalIdClass.java @@ -0,0 +1,17 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.annotations; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target(TYPE) +@Retention(RUNTIME) +public @interface NaturalIdClass { + Class value(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ComponentPropertyHolder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ComponentPropertyHolder.java index ec14d0ac22e8..0dcde51517fd 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ComponentPropertyHolder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ComponentPropertyHolder.java @@ -330,6 +330,12 @@ public Column[] getOverriddenColumn(String propertyName) { result = super.getOverriddenColumn( userPropertyName ); } } + if ( result == null ) { + final String userPropertyName = extractUserPropertyName( "natural_id", propertyName ); + if ( userPropertyName != null ) { + result = super.getOverriddenColumn( userPropertyName ); + } + } if ( result == null ) { final String userPropertyName = extractUserPropertyName( IDENTIFIER_MAPPER_PROPERTY, propertyName ); if ( userPropertyName != null ) { @@ -340,8 +346,8 @@ public Column[] getOverriddenColumn(String propertyName) { } private String extractUserPropertyName(String redundantString, String propertyName) { - String className = component.getOwner().getClassName(); - boolean specialCase = propertyName.startsWith(className) + final String className = component.getOwner().getClassName(); + final boolean specialCase = propertyName.startsWith(className) && propertyName.length() > className.length() + 2 + redundantString.length() // .id. && propertyName.substring( className.length() + 1, className.length() + 1 + redundantString.length() ) .equals(redundantString); diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java index 8506a79245d6..80214d438a7b 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java @@ -365,23 +365,40 @@ private void handleIdentifier( PropertyHolder propertyHolder, Map inheritanceStates, InheritanceState inheritanceState) { - final ElementsToProcess elementsToProcess = inheritanceState.postProcess( persistentClass, this ); - final Set idPropertiesIfIdClass = handleIdClass( + final ElementsToProcess elementsToProcess = + inheritanceState.postProcess( persistentClass, this ); + + processIdPropertiesIfNotAlready( persistentClass, inheritanceState, context, propertyHolder, + handleIdClass( + persistentClass, + inheritanceState, + context, + propertyHolder, + elementsToProcess, + inheritanceStates + ), elementsToProcess, inheritanceStates ); - processIdPropertiesIfNotAlready( + + processNaturalIdPropertiesIfNotAlready( persistentClass, - inheritanceState, - context, - propertyHolder, - idPropertiesIfIdClass, - elementsToProcess, - inheritanceStates +// inheritanceState, +// context, +// propertyHolder, + handleNaturalIdClass( + persistentClass, + inheritanceState, + context, + propertyHolder, + inheritanceStates + ), + elementsToProcess +// inheritanceStates ); } @@ -416,42 +433,43 @@ private Set handleIdClass( ElementsToProcess elementsToProcess, Map inheritanceStates) { final Set idPropertiesIfIdClass = new HashSet<>(); - final boolean isIdClass = mapAsIdClass( - inheritanceStates, - inheritanceState, - persistentClass, - propertyHolder, - elementsToProcess, - idPropertiesIfIdClass, - context - ); + final boolean isIdClass; + // We are looking for @IdClass + // In general we map the id class as identifier using the mapping metadata of the main entity's + // properties and create an identifier mapper containing the id properties of the main entity + final ClassDetails classWithIdClass = inheritanceState.getClassWithIdClass( false ); + if ( classWithIdClass != null ) { + final ClassDetails compositeClass = idClassDetails( inheritanceState, classWithIdClass ); + isIdClass = compositeClass != null + && mapAsIdClass( inheritanceStates, persistentClass, propertyHolder, elementsToProcess, + idPropertiesIfIdClass, context, compositeClass, classWithIdClass ); + } + else { + isIdClass = false; + } if ( !isIdClass ) { wrapIdsInEmbeddedComponents = elementsToProcess.getIdPropertyCount() > 1; } return idPropertiesIfIdClass; } - private boolean mapAsIdClass( - Map inheritanceStates, - InheritanceState inheritanceState, + private Set handleNaturalIdClass( PersistentClass persistentClass, + InheritanceState inheritanceState, + MetadataBuildingContext context, PropertyHolder propertyHolder, - ElementsToProcess elementsToProcess, - Set idPropertiesIfIdClass, - MetadataBuildingContext context) { - // We are looking for @IdClass - // In general we map the id class as identifier using the mapping metadata of the main entity's - // properties and create an identifier mapper containing the id properties of the main entity - final ClassDetails classWithIdClass = inheritanceState.getClassWithIdClass( false ); + Map inheritanceStates) { + final Set naturalIdPropertiesIfNaturalIdClass = new HashSet<>(); + // We are looking for @NaturalIdClass + final ClassDetails classWithIdClass = inheritanceState.getClassWithNaturalIdClass( false ); if ( classWithIdClass != null ) { - final ClassDetails compositeClass = idClassDetails( inheritanceState, classWithIdClass ); - return compositeClass != null - && mapAsIdClass( inheritanceStates, persistentClass, propertyHolder, elementsToProcess, - idPropertiesIfIdClass, context, compositeClass, classWithIdClass ); - } - else { - return false; + final ClassDetails compositeClass = naturalIdClassDetails( inheritanceState, classWithIdClass ); + if ( compositeClass != null ) { + mapAsNaturalIdClass( inheritanceStates, persistentClass, propertyHolder, + naturalIdPropertiesIfNaturalIdClass, context, compositeClass, classWithIdClass ); + } } + return naturalIdPropertiesIfNaturalIdClass; } private boolean mapAsIdClass( @@ -467,8 +485,10 @@ private boolean mapAsIdClass( final TypeDetails classWithIdType = new ClassTypeDetailsImpl( classWithIdClass, TypeDetails.Kind.CLASS ); final AccessType accessType = getPropertyAccessType(); - final PropertyData inferredData = new PropertyPreloadedData( accessType, "id", compositeType ); - final PropertyData baseInferredData = new PropertyPreloadedData( accessType, "id", classWithIdType ); + final PropertyData inferredData = + new PropertyPreloadedData( accessType, "id", compositeType ); + final PropertyData baseInferredData = + new PropertyPreloadedData( accessType, "id", classWithIdType ); final AccessType propertyAccessor = getPropertyAccessor( compositeClass ); // In JPA 2, there is a shortcut if the IdClass is the PK of the associated class pointed to by the id @@ -506,7 +526,7 @@ private boolean mapAsIdClass( compositeType, baseInferredData, propertyAccessor, - true + NavigablePath.IDENTIFIER_MAPPER_PROPERTY ); if ( idClassComponent.isSimpleRecord() ) { mapper.setSimpleRecord( true ); @@ -519,6 +539,55 @@ private boolean mapAsIdClass( } } + private boolean mapAsNaturalIdClass( + Map inheritanceStates, + PersistentClass persistentClass, + PropertyHolder propertyHolder, + Set naturalIdPropertiesIfNaturalIdClass, + MetadataBuildingContext context, + ClassDetails compositeClass, + ClassDetails classWithIdClass) { + final TypeDetails compositeType = new ClassTypeDetailsImpl( compositeClass, TypeDetails.Kind.CLASS ); + final TypeDetails classWithIdType = new ClassTypeDetailsImpl( classWithIdClass, TypeDetails.Kind.CLASS ); + + final AccessType accessType = getPropertyAccessType(); + final PropertyData inferredData = + new PropertyPreloadedData( accessType, "natural_id", compositeType ); + final PropertyData baseInferredData = + new PropertyPreloadedData( accessType, "natural_id", classWithIdType ); + final AccessType propertyAccessor = getPropertyAccessor( compositeClass ); + + final boolean ignoreIdAnnotations = isIgnoreIdAnnotations(); + this.ignoreIdAnnotations = true; + final Component idClassComponent = bindNaturalIdClass( + inferredData, + baseInferredData, + propertyHolder, + propertyAccessor, + context, + inheritanceStates + ); + final Component mapper = createMapperProperty( + inheritanceStates, + persistentClass, + propertyHolder, + context, + classWithIdClass, + compositeType, + baseInferredData, + propertyAccessor, + "_naturalIdMapper" + ); + if ( idClassComponent.isSimpleRecord() ) { + mapper.setSimpleRecord( true ); + } + this.ignoreIdAnnotations = ignoreIdAnnotations; + for ( Property property : mapper.getProperties() ) { + naturalIdPropertiesIfNaturalIdClass.add( property.getName() ); + } + return true; + } + private ClassDetails idClassDetails(InheritanceState inheritanceState, ClassDetails classWithIdClass) { final IdClass idClassAnn = classWithIdClass.getDirectAnnotationUsage( IdClass.class ); final ClassDetailsRegistry classDetailsRegistry = modelsContext().getClassDetailsRegistry(); @@ -537,6 +606,24 @@ private ClassDetails idClassDetails(InheritanceState inheritanceState, ClassDeta } } + private ClassDetails naturalIdClassDetails(InheritanceState inheritanceState, ClassDetails classWithIdClass) { + final NaturalIdClass idClassAnn = classWithIdClass.getDirectAnnotationUsage( NaturalIdClass.class ); + final ClassDetailsRegistry classDetailsRegistry = modelsContext().getClassDetailsRegistry(); + if ( idClassAnn == null ) { + try { + // look for a NaturalId class generated by Hibernate Processor as an inner class of static metamodel + final String generatedIdClassName = inheritanceState.getClassDetails().getClassName() + "_$NaturalId"; + return classDetailsRegistry.resolveClassDetails( generatedIdClassName ); + } + catch (RuntimeException e) { + return null; + } + } + else { + return classDetailsRegistry.resolveClassDetails( idClassAnn.value().getName() ); + } + } + private Component createMapperProperty( Map inheritanceStates, PersistentClass persistentClass, @@ -546,7 +633,7 @@ private Component createMapperProperty( TypeDetails compositeClass, PropertyData baseInferredData, AccessType propertyAccessor, - boolean isIdClass) { + String propertyName) { final Component mapper = createMapper( inheritanceStates, persistentClass, @@ -556,10 +643,10 @@ private Component createMapperProperty( compositeClass, baseInferredData, propertyAccessor, - isIdClass + propertyName ); final Property mapperProperty = new SyntheticProperty(); - mapperProperty.setName( NavigablePath.IDENTIFIER_MAPPER_PROPERTY ); + mapperProperty.setName( propertyName ); mapperProperty.setUpdateable( false ); mapperProperty.setInsertable( false ); mapperProperty.setPropertyAccessorName( "embedded" ); @@ -577,14 +664,10 @@ private Component createMapper( TypeDetails compositeClass, PropertyData baseInferredData, AccessType propertyAccessor, - boolean isIdClass) { + String propertyName) { final Component mapper = fillEmbeddable( propertyHolder, - new PropertyPreloadedData( - propertyAccessor, - NavigablePath.IDENTIFIER_MAPPER_PROPERTY, - compositeClass - ), + new PropertyPreloadedData( propertyAccessor, propertyName, compositeClass ), baseInferredData, propertyAccessor, annotatedClass, @@ -598,12 +681,13 @@ private Component createMapper( null, context, inheritanceStates, - isIdClass + true ); persistentClass.setIdentifierMapper( mapper ); // If id definition is on a mapped superclass, update the mapping - final MappedSuperclass superclass = getMappedSuperclassOrNull( classWithIdClass, inheritanceStates, context ); + final MappedSuperclass superclass = + getMappedSuperclassOrNull( classWithIdClass, inheritanceStates, context ); if ( superclass != null ) { superclass.setDeclaredIdentifierMapper( mapper ); } @@ -713,6 +797,52 @@ private Component bindIdClass( return id; } + private Component bindNaturalIdClass( + PropertyData inferredData, + PropertyData baseInferredData, + PropertyHolder propertyHolder, + AccessType propertyAccessor, + MetadataBuildingContext buildingContext, + Map inheritanceStates) { + propertyHolder.setInIdClass( true ); + + // Fill simple value and property since and NaturalId is a property + final PersistentClass persistentClass = propertyHolder.getPersistentClass(); + if ( !( persistentClass instanceof RootClass rootClass ) ) { + throw new AnnotationException( "Entity '" + persistentClass.getEntityName() + + "' is a subclass in an entity inheritance hierarchy and may not redefine the natural id of the root entity" ); + } + final Component naturalId = fillEmbeddable( + propertyHolder, + inferredData, + baseInferredData, + propertyAccessor, + annotatedClass, + false, + this, + true, + false, + false, + null, + null, + null, + buildingContext, + inheritanceStates, + true + ); +// naturalId.setKey( true ); + if ( naturalId.getPropertySpan() == 0 ) { + throw new AnnotationException( "Class '" + naturalId.getComponentClassName() + + " is the '@NaturalIdClass' for the entity '" + persistentClass.getEntityName() + + "' but has no persistent properties" ); + } + + rootClass.setNaturalId( naturalId ); + rootClass.setEmbeddedIdentifier( inferredData.getPropertyType() == null ); + propertyHolder.setInIdClass( null ); + return naturalId; + } + private void handleSecondaryTables() { annotatedClass.forEachRepeatedAnnotationUsages( JpaAnnotations.SECONDARY_TABLE, modelsContext(), usage -> addSecondaryTable( usage, null, false ) ); @@ -1081,6 +1211,43 @@ else if ( !missingEntityProperties.isEmpty() ) { } } + private void processNaturalIdPropertiesIfNotAlready( + PersistentClass persistentClass, +// InheritanceState inheritanceState, +// MetadataBuildingContext context, +// PropertyHolder propertyHolder, + Set naturalIdPropertiesIfNaturalIdClass, + ElementsToProcess elementsToProcess) { +// Map inheritanceStates) { + final Set missingIdProperties = new HashSet<>( naturalIdPropertiesIfNaturalIdClass ); + final Set missingEntityProperties = new HashSet<>(); + for ( PropertyData propertyAnnotatedElement : elementsToProcess.getElements() ) { + final String propertyName = propertyAnnotatedElement.getPropertyName(); + if ( !naturalIdPropertiesIfNaturalIdClass.contains( propertyName ) ) { + final MemberDetails property = propertyAnnotatedElement.getAttributeMember(); + final boolean hasNaturalIdAnnotation = property.hasDirectAnnotationUsage( NaturalId.class ); + if ( !naturalIdPropertiesIfNaturalIdClass.isEmpty() && !isIgnoreIdAnnotations() && hasNaturalIdAnnotation ) { + missingEntityProperties.add( propertyName ); + } + // TODO: any work to do here? + } + else { + missingIdProperties.remove( propertyName ); + } + } + + if ( !missingIdProperties.isEmpty() ) { + throw new AnnotationException( "Entity '" + persistentClass.getEntityName() + + "' has an '@NaturalIdClass' with properties " + getMissingPropertiesString( missingIdProperties ) + + " which do not match properties of the entity class" ); + } + else if ( !missingEntityProperties.isEmpty() ) { + throw new AnnotationException( "Entity '" + persistentClass.getEntityName() + + "' has '@NaturalId' annotated properties " + getMissingPropertiesString( missingEntityProperties ) + + " which do not match properties of the specified '@IdClass'" ); + } + } + private static String getMissingPropertiesString(Set propertyNames) { final StringBuilder missingProperties = new StringBuilder(); for ( String propertyName : propertyNames ) { diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/InheritanceState.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/InheritanceState.java index 862d60b3ec65..3a54daf1921e 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/InheritanceState.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/InheritanceState.java @@ -10,6 +10,8 @@ import java.util.stream.Stream; import org.hibernate.AnnotationException; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.NaturalIdClass; import org.hibernate.boot.spi.AccessType; import org.hibernate.boot.spi.InFlightMetadataCollector; import org.hibernate.boot.spi.MetadataBuildingContext; @@ -200,6 +202,28 @@ else if ( classDetails.hasDirectAnnotationUsage( IdClass.class ) ) { } } + public ClassDetails getClassWithNaturalIdClass(boolean evenIfSubclass) { + if ( !evenIfSubclass && hasParents() ) { + return null; + } + else if ( classDetails.hasDirectAnnotationUsage( NaturalIdClass.class ) ) { + return classDetails; + } + else { + final long count = + Stream.concat( classDetails.getFields().stream(), classDetails.getMethods().stream() ) + .filter( member -> member.hasDirectAnnotationUsage( NaturalId.class ) ) + .count(); + if ( count > 1 ) { + return classDetails; + } + else { + final InheritanceState state = getSuperclassInheritanceState( classDetails, inheritanceStatePerClass ); + return state == null ? null : state.getClassWithIdClass( true ); + } + } + } + public Boolean hasIdClassOrEmbeddedId() { if ( hasIdClassOrEmbeddedId == null ) { hasIdClassOrEmbeddedId = false; diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/NaturalIdBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/NaturalIdBinder.java index ac924887708c..03138f849c82 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/NaturalIdBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/NaturalIdBinder.java @@ -34,8 +34,8 @@ static void addNaturalIds( MetadataBuildingContext context) { // Natural ID columns must reside in one single UniqueKey within the Table. // For now, simply ensure consistent naming. - final NaturalId naturalId = property.getDirectAnnotationUsage( NaturalId.class ); - if ( naturalId != null ) { + if ( property.hasDirectAnnotationUsage( NaturalId.class ) + && !columns.getPropertyHolder().getPath().endsWith( ".natural_id" ) ) { final AnnotatedColumns annotatedColumns = joinColumns != null ? joinColumns : columns; final Identifier name = uniqueKeyName( context, annotatedColumns ); if ( inSecondPass ) { diff --git a/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java b/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java index 09274592ec8a..738645a6a150 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java @@ -34,6 +34,7 @@ import org.hibernate.FetchNotFoundException; import org.hibernate.FlushMode; import org.hibernate.HibernateException; +import org.hibernate.IdentifierLoadAccess; import org.hibernate.Interceptor; import org.hibernate.JDBCException; import org.hibernate.LobHelper; @@ -108,9 +109,11 @@ import org.hibernate.event.spi.RefreshEventListener; import org.hibernate.event.spi.ReplicateEvent; import org.hibernate.event.spi.ReplicateEventListener; +import org.hibernate.internal.util.ReflectHelper; import org.hibernate.loader.internal.CacheLoadHelper; import org.hibernate.metamodel.model.domain.EntityDomainType; import org.hibernate.metamodel.model.domain.ManagedDomainType; +import org.hibernate.query.BindableType; import org.hibernate.resource.transaction.spi.TransactionObserver; import org.hibernate.event.monitor.spi.EventMonitor; import org.hibernate.event.monitor.spi.DiagnosticEvent; @@ -148,6 +151,7 @@ import org.hibernate.stat.SessionStatistics; import org.hibernate.stat.internal.SessionStatisticsImpl; import org.hibernate.stat.spi.StatisticsImplementor; +import org.hibernate.type.CompositeType; import org.hibernate.type.descriptor.WrapperOptions; import jakarta.persistence.CacheRetrieveMode; @@ -2442,10 +2446,7 @@ private T find(Class entityClass, Object primaryKey, LockOptions lockOpti try { loadQueryInfluencers.getEffectiveEntityGraph().applyConfiguredGraph( properties ); loadQueryInfluencers.setReadOnly( readOnlyHint( properties ) ); - return byId( entityClass ) - .with( determineAppropriateLocalCacheMode( properties ) ) - .with( lockOptions ) - .load( primaryKey ); + return findByIdOrNaturalId( entityClass, primaryKey, lockOptions, properties ); } catch ( FetchNotFoundException e ) { // This may happen if the entity has an associations mapped with @@ -2500,6 +2501,44 @@ private T find(Class entityClass, Object primaryKey, LockOptions lockOpti } } + private T findByIdOrNaturalId( + Class entityClass, Object primaryKey, LockOptions lockOptions, Map properties) { + final CompositeType naturalIdType = + requireEntityPersister( entityClass ).getEntityMetamodel().getNaturalIdType(); + if ( naturalIdType instanceof BindableType bindableType && bindableType.isInstance( primaryKey ) ) { + return byNaturalId( entityClass ) + .with( lockOptions ) + .using( deconstructNaturalId( primaryKey, naturalIdType ) ) + .load(); + } + else { + return byId( entityClass ) + .with( determineAppropriateLocalCacheMode( properties ) ) + .with( lockOptions ) + .load( primaryKey ); + } + } + + private Map deconstructNaturalId(Object primaryKey, CompositeType naturalIdType) { + final String[] propertyNames = naturalIdType.getPropertyNames(); +// final Object[] propertyValues = naturalIdType.getPropertyValues( primaryKey ); + final Map map = new HashMap<>(); + for ( int i = 0; i < propertyNames.length; i++ ) { + map.put( propertyNames[i], getValue( primaryKey, naturalIdType, propertyNames[i] ) ); +// propertyValues[i] ); + } + return map; + } + + private static Object getValue(Object primaryKey, CompositeType naturalIdType, String propertyNames) { + try { + return ReflectHelper.findField( naturalIdType.getReturnedClass(), propertyNames ).get( primaryKey ); + } + catch (IllegalAccessException e) { + throw new RuntimeException( e ); + } + } + private static void logIgnoringEntityNotFound(Class entityClass, Object primaryKey) { if ( log.isDebugEnabled() ) { log.ignoringEntityNotFound( @@ -2509,7 +2548,7 @@ private static void logIgnoringEntityNotFound(Class entityClass, Object p } } - private void setLoadAccessOptions(FindOption[] options, IdentifierLoadAccessImpl loadAccess) { + private void setLoadAccessOptions(FindOption[] options, IdentifierLoadAccess loadAccess) { CacheStoreMode storeMode = getCacheStoreMode(); CacheRetrieveMode retrieveMode = getCacheRetrieveMode(); LockOptions lockOptions = copySessionLockOptions(); @@ -2551,7 +2590,7 @@ else if ( option instanceof ReadOnlyMode ) { @Override public T find(Class entityClass, Object primaryKey, FindOption... options) { - final IdentifierLoadAccessImpl loadAccess = byId( entityClass ); + final IdentifierLoadAccess loadAccess = byId( entityClass ); setLoadAccessOptions( options, loadAccess ); return loadAccess.load( primaryKey ); } @@ -2560,7 +2599,7 @@ public T find(Class entityClass, Object primaryKey, FindOption... options public T find(EntityGraph entityGraph, Object primaryKey, FindOption... options) { final RootGraph graph = (RootGraph) entityGraph; final ManagedDomainType type = graph.getGraphedType(); - final IdentifierLoadAccessImpl loadAccess = + final IdentifierLoadAccess loadAccess = switch ( type.getRepresentationMode() ) { case MAP -> byId( type.getTypeName() ); case POJO -> byId( type.getJavaType() ); diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/PersistentClass.java b/hibernate-core/src/main/java/org/hibernate/mapping/PersistentClass.java index 6461a56be2b9..174e10f44c78 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/PersistentClass.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/PersistentClass.java @@ -1208,4 +1208,6 @@ public Supplier getDeleteExpectation() { public void setDeleteExpectation(Supplier deleteExpectation) { this.deleteExpectation = deleteExpectation; } + + public abstract Component getNaturalId(); } diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/RootClass.java b/hibernate-core/src/main/java/org/hibernate/mapping/RootClass.java index d7e9ed3c1f45..f91ccd970d78 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/RootClass.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/RootClass.java @@ -51,6 +51,7 @@ public final class RootClass extends PersistentClass implements TableOwner, Soft private Property declaredVersion; private Column softDeleteColumn; private SoftDeleteType softDeleteStrategy; + private Component naturalId; public RootClass(MetadataBuildingContext buildingContext) { super( buildingContext ); @@ -222,7 +223,15 @@ public void setIdentifier(KeyValue identifier) { public void setIdentifierProperty(Property identifierProperty) { this.identifierProperty = identifierProperty; identifierProperty.setPersistentClass( this ); + } + + @Override + public Component getNaturalId() { + return naturalId; + } + public void setNaturalId(Component naturalId) { + this.naturalId = naturalId; } public void setMutable(boolean mutable) { diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/Subclass.java b/hibernate-core/src/main/java/org/hibernate/mapping/Subclass.java index 07163c3940f6..6a727049622a 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/Subclass.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/Subclass.java @@ -291,4 +291,9 @@ public Component getIdentifierMapper() { public OptimisticLockStyle getOptimisticLockStyle() { return superclass.getOptimisticLockStyle(); } + + @Override + public Component getNaturalId() { + return superclass.getNaturalId(); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/MappingMetamodelImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/MappingMetamodelImpl.java index eaafd6a76505..4ca9f3103222 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/MappingMetamodelImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/MappingMetamodelImpl.java @@ -203,7 +203,10 @@ private void registerEmbeddableMappingType(MetadataImplementor bootModel) { composite -> { final ComponentType compositeType = (ComponentType) composite.getType(); final EmbeddableValuedModelPart mappingModelPart = compositeType.getMappingModelPart(); - embeddableValuedModelPart.put( mappingModelPart.getNavigableRole(), mappingModelPart ); + if ( mappingModelPart != null ) { + embeddableValuedModelPart.put( mappingModelPart.getNavigableRole(), mappingModelPart ); + } + // TODO: consider @NaturalIdClass } ); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/BindableType.java b/hibernate-core/src/main/java/org/hibernate/query/BindableType.java index f81c68852cfc..c9bb89d2f325 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/BindableType.java +++ b/hibernate-core/src/main/java/org/hibernate/query/BindableType.java @@ -22,7 +22,12 @@ public interface BindableType { */ Class getBindableJavaType(); - default boolean isInstance(J value) { + /** + * Determine if the given value is an instance of this type. + * @param value any Java object + * @return {@code true} is the given value is an instance of the type + */ + default boolean isInstance(Object value) { return getBindableJavaType().isInstance( value ); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/SqmExpressible.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/SqmExpressible.java index bbfba047505c..5289fa09c02e 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/SqmExpressible.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/SqmExpressible.java @@ -29,7 +29,7 @@ default JavaType getRelationalJavaType() { } @Override - default boolean isInstance(J value) { + default boolean isInstance(Object value) { return getExpressibleJavaType().isInstance( value ); } diff --git a/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java b/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java index 9d167ee00986..a7cd932c1206 100644 --- a/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java +++ b/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java @@ -87,6 +87,7 @@ public class EntityMetamodel implements Serializable { private final int subclassId; private final IdentifierProperty identifierAttribute; private final boolean versioned; + private final CompositeType naturalIdType; private final int propertySpan; private final int versionPropertyIndex; @@ -178,6 +179,9 @@ public EntityMetamodel( versioned = persistentClass.isVersioned(); + final Component naturalId = persistentClass.getNaturalId(); + naturalIdType = naturalId == null ? null : naturalId.getType(); + final boolean collectionsInDefaultFetchGroupEnabled = creationContext.getSessionFactoryOptions().isCollectionsInDefaultFetchGroupEnabled(); @@ -584,6 +588,10 @@ private void mapPropertyToIndex(Property property, int i) { } } + public CompositeType getNaturalIdType() { + return naturalIdType; + } + /** * @return {@code true} if one of the properties belonging to the natural id * is generated during the execution of an {@code insert} statement 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 829eb885267e..8bece9e1bc28 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/ComponentType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/ComponentType.java @@ -434,8 +434,8 @@ public Object[] getPropertyValues(Object component) { return new Object[propertySpan + discriminatorColumnSpan]; } else if ( component instanceof Object[] ) { - // A few calls to hashCode pass the property values already in an - // Object[] (ex: QueryKey hash codes for cached queries). + // A few calls to hashCode pass the property values already in + // an Object[] (ex: QueryKey hash codes for cached queries). // It's easiest to just check for the condition here prior to // trying reflection. return (Object[]) component; @@ -815,11 +815,13 @@ private ValueExtractor jdbcValueExtractor() { } protected final EmbeddableInstantiator instantiator(Object compositeInstance) { - final EmbeddableRepresentationStrategy representationStrategy = embeddableTypeDescriptor().getRepresentationStrategy(); + final EmbeddableRepresentationStrategy representationStrategy = + embeddableTypeDescriptor().getRepresentationStrategy(); if ( embeddableTypeDescriptor().isPolymorphic() ) { - final String compositeClassName = compositeInstance != null ? - compositeInstance.getClass().getName() : - componentClass.getName(); + final String compositeClassName = + compositeInstance != null + ? compositeInstance.getClass().getName() + : componentClass.getName(); return representationStrategy.getInstantiatorForClass( compositeClassName ); } else { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/naturalid/idclass/NaturalIdClassTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/naturalid/idclass/NaturalIdClassTest.java new file mode 100644 index 000000000000..f92a66988a2f --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/naturalid/idclass/NaturalIdClassTest.java @@ -0,0 +1,61 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.mapping.naturalid.idclass; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.NaturalIdClass; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@Jpa(annotatedClasses = NaturalIdClassTest.Person.class) +public class NaturalIdClassTest { + @Test void test(EntityManagerFactoryScope scope) { + scope.inTransaction( em -> { + var entity = new Person(); + entity.firstName = "Gavin"; + entity.lastName = "King"; + em.persist( entity ); + } ); + scope.inTransaction( em -> { + var entity = em.find( Person.class, new Name("Gavin", "King") ); + assertNotNull( entity ); + } ); + } + + static class Name { + String firstName; + String lastName; + Name(String firstName, String lastName) { + this.firstName = firstName; + this.lastName = lastName; + } + } + + static class PersonId { + String id; + String country; + PersonId(String id, String country) { + this.id = id; + this.country = country; + } + PersonId() {} + } + + @Entity + @IdClass(PersonId.class) + @NaturalIdClass(Name.class) + static class Person { + @Id String id; + @Id String country; + @NaturalId String firstName; + @NaturalId String lastName; + } +}