diff --git a/documentation/src/main/asciidoc/userguide/chapters/domain/natural_id.adoc b/documentation/src/main/asciidoc/userguide/chapters/domain/natural_id.adoc index 7d00d39e7e..9e01dc9942 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/domain/natural_id.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/domain/natural_id.adoc @@ -5,7 +5,15 @@ Natural ids represent domain model unique identifiers that have a meaning in the real world too. Even if a natural id does not make a good primary key (surrogate keys being usually preferred), it's still useful to tell Hibernate about it. -As we will see later, Hibernate provides a dedicated, efficient API for loading an entity by its natural id much like it offers for loading by its identifier (PK). +As we will see later, Hibernate provides a dedicated, efficient API for loading an entity by its natural id much like it offers for loading by identifier (PK). + +[IMPORTANT] +==== +All values used in a natural id must be non-nullable. + +For natural id mappings using a to-one association, this precludes the use of not-found +mappings which effectively define a nullable mapping. +==== [[naturalid-mapping]] ==== Natural Id Mapping 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 22b62c4ff3..40274ebb19 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 @@ -19,6 +19,8 @@ import java.util.Set; import org.hibernate.EntityMode; import org.hibernate.HibernateException; import org.hibernate.MappingException; +import org.hibernate.annotations.NotFoundAction; +import org.hibernate.boot.spi.MetadataImplementor; import org.hibernate.bytecode.enhance.spi.interceptor.EnhancementHelper; import org.hibernate.bytecode.spi.BytecodeEnhancementMetadata; import org.hibernate.cfg.NotYetImplementedException; @@ -30,8 +32,15 @@ import org.hibernate.internal.CoreMessageLogger; import org.hibernate.internal.util.ReflectHelper; import org.hibernate.internal.util.collections.ArrayHelper; import org.hibernate.mapping.Component; +import org.hibernate.mapping.GeneratorCreator; +import org.hibernate.mapping.ManyToOne; import org.hibernate.mapping.PersistentClass; import org.hibernate.mapping.Property; +import org.hibernate.mapping.Subclass; +import org.hibernate.mapping.ToOne; +import org.hibernate.mapping.Value; +import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.tuple.GenerationTiming; import org.hibernate.tuple.IdentifierProperty; @@ -231,6 +240,7 @@ public class EntityMetamodel implements Serializable { } if ( prop.isNaturalIdentifier() ) { + verifyNaturalIdProperty( property ); naturalIdNumbers.add( i ); if ( prop.isUpdateable() ) { foundUpdateableNaturalIdProperty = true; @@ -658,6 +668,29 @@ public class EntityMetamodel implements Serializable { } } + private void verifyNaturalIdProperty(Property property) { + final Value value = property.getValue(); + if ( value instanceof ManyToOne ) { + final ManyToOne toOne = (ManyToOne) value; + if ( toOne.getNotFoundAction() == NotFoundAction.IGNORE ) { + throw new MappingException( + "Attribute marked as natural-id can not also be a not-found association - " + + propertyName( property ) + ); + } + } + else if ( value instanceof Component ) { + final Component component = (Component) value; + for ( Property componentProperty : component.getProperties() ) { + verifyNaturalIdProperty( componentProperty ); + } + } + } + + private String propertyName(Property property) { + return getName() + "." + property.getName(); + } + private static class NoInMemoryValueGenerationStrategy implements InMemoryValueGenerationStrategy { /** * Singleton access diff --git a/hibernate-core/src/test/java/org/hibernate/test/naturalid/ValidationTests.java b/hibernate-core/src/test/java/org/hibernate/test/naturalid/ValidationTests.java new file mode 100644 index 0000000000..54cea3fdbb --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/naturalid/ValidationTests.java @@ -0,0 +1,105 @@ +/* + * 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.orm.test.mapping.naturalid; + +import org.hibernate.MappingException; +import org.hibernate.SessionFactory; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.NotFound; +import org.hibernate.annotations.NotFoundAction; +import org.hibernate.boot.MetadataSources; +import org.hibernate.boot.registry.StandardServiceRegistry; + +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.ServiceRegistryScope; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * @author Steve Ebersole + */ +@ServiceRegistry +public class ValidationTests { + @Test + void checkManyToOne(ServiceRegistryScope registryScope) { + final StandardServiceRegistry registry = registryScope.getRegistry(); + final MetadataSources metadataSources = new MetadataSources( registry ) + .addAnnotatedClass( Thing1.class ) + .addAnnotatedClass( Thing2.class ); + try (final SessionFactory sessionFactory = metadataSources.buildMetadata().buildSessionFactory(); ) { + fail( "Expecting an exception" ); + } + catch (MappingException expected) { + assertThat( expected.getMessage() ) + .startsWith( "Attribute marked as natural-id can not also be a not-found association - " ); + } + } + + @Test + void checkEmbeddable(ServiceRegistryScope registryScope) { + final StandardServiceRegistry registry = registryScope.getRegistry(); + final MetadataSources metadataSources = new MetadataSources( registry ) + .addAnnotatedClass( Thing1.class ) + .addAnnotatedClass( Thing3.class ) + .addAnnotatedClass( Container.class ); + try (final SessionFactory sessionFactory = metadataSources.buildMetadata().buildSessionFactory(); ) { + fail( "Expecting an exception" ); + } + catch (MappingException expected) { + assertThat( expected.getMessage() ) + .startsWith( "Attribute marked as natural-id can not also be a not-found association - " ); + } + } + + @Entity(name="Thing1") + @Table(name="thing_1") + public static class Thing1 { + @Id + private Integer id; + private String name; + } + + @Entity(name="Thing2") + @Table(name="thing_2") + public static class Thing2 { + @Id + private Integer id; + private String name; + @NaturalId + @ManyToOne + @NotFound(action = NotFoundAction.IGNORE) + private Thing1 thing1; + } + + @Embeddable + public static class Container { + @NaturalId + @ManyToOne + @NotFound(action = NotFoundAction.IGNORE) + private Thing1 thing1; + } + + @Entity(name="Thing2") + @Table(name="thing_2") + public static class Thing3 { + @Id + private Integer id; + private String name; + @NaturalId + @Embedded + private Container container; + } +}