natural-id + not-found

https://hibernate.atlassian.net/browse/HHH-17197 - Add check for illegal combo of to-one + natural-id + not-found
https://hibernate.atlassian.net/browse/HHH-17196 - Documentation for @NaturalId should be more explicit about non-nullability

(cherry picked from commit 6c2e04381d)
This commit is contained in:
Steve Ebersole 2023-09-25 17:24:17 -05:00
parent e14c7f8740
commit 8f4afa625f
3 changed files with 147 additions and 1 deletions

View File

@ -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

View File

@ -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

View File

@ -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;
}
}