8000 natural-id + not-found · hibernate/hibernate-orm@8f4afa6 · GitHub
[go: up one dir, main page]

Skip to content

Commit 8f4afa6

Browse files
committed
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 6c2e043)
1 parent e14c7f8 commit 8f4afa6

File tree

3 files changed

+147
-1
lines changed

3 files changed

+147
-1
lines changed

documentation/src/main/asciidoc/userguide/chapters/domain/natural_id.adoc

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,15 @@
55

66
Natural ids represent domain model unique identifiers that have a meaning in the real world too.
77
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.
8-
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).
8+
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).
9+
10+
[IMPORTANT]
11+
====
12+
All values used in a natural id must be non-nullable.
13+
14+
For natural id mappings using a to-one association, this precludes the use of not-found
15+
mappings which effectively define a nullable mapping.
16+
====
917

1018
[[naturalid-mapping]]
1119
==== Natural Id Mapping

hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import org.hibernate.EntityMode;
2020
import org.hibernate.HibernateException;
2121
import org.hibernate.MappingException;
22+
import org.hibernate.annotations.NotFoundAction;
23+
import org.hibernate.boot.spi.MetadataImplementor;
2224
import org.hibernate.bytecode.enhance.spi.interceptor.EnhancementHelper;
2325
import org.hibernate.bytecode.spi.BytecodeEnhancementMetadata;
2426
import org.hibernate.cfg.NotYetImplementedException;
@@ -30,8 +32,15 @@
3032
import org.hibernate.internal.util.ReflectHelper;
3133
import org.hibernate.internal.util.collections.ArrayHelper;
3234
import org.hibernate.mapping.Component;
35+
import org.hibernate.mapping.GeneratorCreator;
36+
import org.hibernate.mapping.ManyToOne;
3337
import org.hibernate.mapping.PersistentClass;
3438
import org.hibernate.mapping.Property;
39+
import org.hibernate.mapping.Subclass;
40+
import org.hibernate.mapping.ToOne;
41+
import org.hibernate.mapping.Value;
42+
import org.hibernate.metamodel.mapping.EntityMappingType;
43+
import org.hibernate.metamodel.spi.RuntimeModelCreationContext;
3544
import org.hibernate.persister.entity.EntityPersister;
3645
import org.hibernate.tuple.GenerationTiming;
3746
import org.hibernate.tuple.IdentifierProperty;
@@ -231,6 +240,7 @@ public EntityMetamodel(
231240
}
232241

233242
if ( prop.isNaturalIdentifier() ) {
243+
verifyNaturalIdProperty( property );
234244
naturalIdNumbers.add( i );
235245
if ( prop.isUpdateable() ) {
236246
foundUpdateableNaturalIdProperty = true;
@@ -658,6 +668,29 @@ else if ( hadInDatabaseGeneration ) {
658668
}
659669
}
660670

671+
private void verifyNaturalIdProperty(Property property) {
672+
final Value value = property.getValue();
673+
if ( value instanceof ManyToOne ) {
674+
final ManyToOne toOne = (ManyToOne) value;
675+
if ( toOne.getNotFoundAction() == NotFoundAction.IGNORE ) {
676+
throw new MappingException(
677+
"Attribute marked as natural-id can not also be a not-found association - "
678+
+ propertyName( property )
679+
);
680+
}
681+
}
682+
else if ( value instanceof Component ) {
683+
final Component component = (Component) value;
684+
for ( Property componentProperty : component.getProperties() ) {
685+
verifyNaturalIdProperty( componentProperty );
686+
}
687+
}
688+
}
689+
690+
private String propertyName(Property property) {
691+
return getName() + "." + property.getName();
692+
}
693+
661694
private static class NoInMemoryValueGenerationStrategy implements InMemoryValueGenerationStrategy {
662695
/**
663696
* Singleton access
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
* Hibernate, Relational Persistence for Idiomatic Java
3+
*
4+
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
5+
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html.
6+
*/
7+
package org.hibernate.orm.test.mapping.naturalid;
8+
9+
import org.hibernate.MappingException;
10+
import org.hibernate.SessionFactory;
11+
import org.hibernate.annotations.NaturalId;
12+
import org.hibernate.annotations.NotFound;
13+
import org.hibernate.annotations.NotFoundAction;
14+
import org.hibernate.boot.MetadataSources;
15+
import org.hibernate.boot.registry.StandardServiceRegistry;
16+
17+
import org.hibernate.testing.orm.junit.ServiceRegistry;
18+
import org.hibernate.testing.orm.junit.ServiceRegistryScope;
19+
import org.junit.jupiter.api.Test;
20+
21+
import jakarta.persistence.Embeddable;
22+
import jakarta.persistence.Embedded;
23+
import jakarta.persistence.Entity;
24+
import jakarta.persistence.Id;
25+
import jakarta.persistence.ManyToOne;
26+
import jakarta.persistence.Table;
27+
28+
import static org.assertj.core.api.Assertions.assertThat;
29+
import static org.junit.jupiter.api.Assertions.fail;
30+
31+
/**
32+
* @author Steve Ebersole
33+
*/
34+
@ServiceRegistry
35+
public class ValidationTests {
36+
@Test
37+
void checkManyToOne(ServiceRegistryScope registryScope) {
38+
final StandardServiceRegistry registry = registryScope.getRegistry();
39+
final MetadataSources metadataSources = new MetadataSources( registry )
40+
.addAnnotatedClass( Thing1.class )
41+
.addAnnotatedClass( Thing2.class );
42+
try (final SessionFactory sessionFactory = metadataSources.buildMetadata().buildSessionFactory(); ) {
43+
fail( "Expecting an exception" );
44+
}
45+
catch (MappingException expected) {
46+
assertThat( expected.getMessage() )
47+
.startsWith( "Attribute marked as natural-id can not also be a not-found association - " );
48+
}
49+
}
50+
51+
@Test
52+
void checkEmbeddable(ServiceRegistryScope registryScope) {
53+
final StandardServiceRegistry registry = registryScope.getRegistry();
54+
final MetadataSources metadataSources = new MetadataSources( registry )
55+
.addAnnotatedClass( Thing1.class )
56+
.addAnnotatedClass( Thing3.class )
57+
.addAnnotatedClass( Container.class );
58+
try (final SessionFactory sessionFactory = metadataSources.buildMetadata().buildSessionFactory(); ) {
59+
fail( "Expecting an exception" );
60+
}
61+
catch (MappingException expected) {
62+
assertThat( expected.getMessage() )
63+
.startsWith( "Attribute marked as natural-id can not also be a not-found association - " );
64+
}
65+
}
66+
67+
@Entity(name="Thing1")
68+
@Table(name="thing_1")
69+
public static class Thing1 {
70+
@Id
71+
private Integer id;
72+
private String name;
73+
}
74+
75+
@Entity(name="Thing2")
76+
@Table(name="thing_2")
77+
public static class Thing2 {
78+
@Id
79+
private Integer id;
80+
private String name;
81+
@NaturalId
82+
@ManyToOne
83+
@NotFound(action = NotFoundAction.IGNORE)
84+
private Thing1 thing1;
85+
}
86+
87+
@Embeddable
88+
public static class Container {
89+
@NaturalId
90+
@ManyToOne
91+
@NotFound(action = NotFoundAction.IGNORE)
92+
private Thing1 thing1;
93+
}
94+
95+
@Entity(name="Thing2")
96+
@Table(name="thing_2")
97+
public static class Thing3 {
98+
@Id
99+
private Integer id;
100+
private String name;
101+
@NaturalId
102+
@Embedded
103+
private Container container;
104+
}
105+
}

0 commit comments

Comments
 (0)
0