Skip to content

Commit ee2b290

Browse files
committed
HHH-16383 - NaturalIdClass
1 parent 224b579 commit ee2b290

23 files changed

+264
-162
lines changed
Lines changed: 69 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,15 @@
11
[[naturalid]]
22
=== Natural Ids
33
:core-project-dir: {root-project-dir}/hibernate-core
4-
:example-dir-naturalid: {core-project-dir}/src/test/java/org/hibernate/orm/test/mapping/identifier
4+
:core-test-base-dir: {core-project-dir}/src/test/java/org/hibernate/orm/test
5+
:example-dir-naturalid: {core-test-base-dir}/mapping/identifier
56
:jcache-project-dir: {root-project-dir}/hibernate-jcache
67
:example-dir-caching: {jcache-project-dir}/src/test/java/org/hibernate/orm/test/caching
78
:extrasdir: extras
89

9-
Natural ids represent domain model unique identifiers that have a meaning in the real world too.
10+
Natural ids are unique identifiers in the domain model that have a meaning in the real world too.
1011
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.
11-
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).
12-
13-
[IMPORTANT]
14-
====
15-
All values used in a natural id must be non-nullable.
16-
17-
For natural id mappings using a to-one association, this precludes the use of not-found
18-
mappings which effectively define a nullable mapping.
19-
====
12+
As we will see <<find-by-natural-id,later>>, Hibernate provides an efficient means for loading an entity by its natural id much like it offers for loading by identifier (PK).
2013

2114
[[naturalid-mapping]]
2215
==== Natural Id Mapping
@@ -50,104 +43,112 @@ include::{example-dir-naturalid}/MultipleNaturalIdTest.java[tags=naturalid-multi
5043
----
5144
====
5245

53-
[[naturalid-api]]
54-
==== Natural Id API
55-
56-
As stated before, Hibernate provides an API for loading entities by their associated natural id.
57-
This is represented by the `org.hibernate.NaturalIdLoadAccess` contract obtained via Session#byNaturalId.
46+
Natural ids defined using multiple persistent attributes may also define a link:{doc-javadoc-url}org/hibernate/annotations/NaturalIdClass.html[`@NaturalIdClass`] which can be used for <<find-by-natural-id,find operations>>.
5847

59-
[NOTE]
60-
====
61-
If the entity does not define a natural id, trying to load an entity by its natural id will throw an exception.
62-
====
63-
64-
[[naturalid-load-access-example]]
65-
.Using NaturalIdLoadAccess
48+
[[naturalidclass-example]]
49+
.Natural id with @NaturalIdClass
6650
====
6751
[source,java]
6852
----
69-
include::{example-dir-naturalid}/SimpleNaturalIdTest.java[tags=naturalid-load-access-example,indent=0]
53+
include::{core-test-base-dir}/mapping/naturalid/idclass/SimpleNaturalIdClassTests.java[tags=naturalidclass-mapping-example,indent=0]
7054
----
55+
====
7156

72-
[source,java]
73-
----
74-
include::{example-dir-naturalid}/CompositeNaturalIdTest.java[tags=naturalid-load-access-example,indent=0]
75-
----
7657

58+
[[natural-id-mutability]]
59+
==== Natural id mutability
60+
61+
A natural id may be mutable or immutable. By default, the `@NaturalId` annotation marks the attribute as immutable.
62+
An immutable natural id is expected to never change its value.
63+
In fact, Hibernate will check at flush-time to ensure that the value has not been altered.
64+
65+
If the value(s) of the natural id attribute(s) may change, `@NaturalId(mutable = true)` should be used instead.
66+
67+
[[naturalid-mutable-mapping-example]]
68+
.Mutable natural id mapping
69+
====
7770
[source,java]
7871
----
79-
include::{example-dir-naturalid}/MultipleNaturalIdTest.java[tags=naturalid-load-access-example,indent=0]
72+
include::{example-dir-naturalid}/MutableNaturalIdTest.java[tags=naturalid-mutable-mapping-example,indent=0]
8073
----
8174
====
8275

83-
NaturalIdLoadAccess offers 2 distinct methods for obtaining the entity:
8476

85-
`load()`:: obtains a reference to the entity, making sure that the entity state is initialized.
86-
`getReference()`:: obtains a reference to the entity. The state may or may not be initialized.
87-
If the entity is already associated with the current running Session, that reference (loaded or not) is returned.
88-
If the entity is not loaded in the current Session and the entity supports proxy generation, an uninitialized proxy is generated and returned, otherwise the entity is loaded from the database and returned.
8977

90-
`NaturalIdLoadAccess` allows loading an entity by natural id and at the same time applies a pessimistic lock.
91-
For additional details on locking, see the <<chapters/locking/Locking.adoc#locking,Locking>> chapter.
9278

93-
We will discuss the last method available on NaturalIdLoadAccess ( `setSynchronizationEnabled()` ) in <<naturalid-mutability-caching>>.
9479

95-
Because the `Book` entities in the first two examples define "simple" natural ids, we can load them as follows:
9680

97-
[[naturalid-simple-load-access-example]]
98-
.Loading by simple natural id
81+
82+
83+
84+
85+
86+
87+
88+
89+
90+
91+
92+
[[natural-id-caching]]
93+
==== Natural id resolution caching
94+
95+
Within the Session, Hibernate maintains a cross reference of the resolutions from natural id values to entity identifier (PK) values.
96+
We can also have this value resolution cached in the second level cache if second level caching is enabled.
97+
98+
[[naturalid-caching]]
99+
.Natural id caching
99100
====
100101
[source,java]
101102
----
102-
include::{example-dir-naturalid}/SimpleNaturalIdTest.java[tags=naturalid-simple-load-access-example,indent=0]
103+
include::{example-dir-caching}/CacheableNaturalIdTest.java[tags=naturalid-cacheable-mapping-example,indent=0]
103104
----
105+
====
104106

105-
[source,java]
106-
----
107-
include::{example-dir-naturalid}/CompositeNaturalIdTest.java[tags=naturalid-simple-load-access-example,indent=0]
108-
----
107+
[IMPORTANT]
108+
====
109+
Think carefully before caching resolutions for natural ids which are partially or fully <<natural-id-mutability,mutable>> in the second level cache as this will often have a negative impact on performance.
109110
====
110111

111-
Here we see the use of the `org.hibernate.SimpleNaturalIdLoadAccess` contract,
112-
obtained via `Session#bySimpleNaturalId()`.
113112

114-
`SimpleNaturalIdLoadAccess` is similar to `NaturalIdLoadAccess` except that it does not define the using method.
115-
Instead, because these _simple_ natural ids are defined based on just one attribute we can directly pass
116-
the corresponding natural id attribute value directly to the `load()` and `getReference()` methods.
113+
114+
[[find-by-natural-id]]
115+
[[naturalid-api]]
116+
==== Loading by natural id
117+
118+
Hibernate provides a means to load one or more entities by natural id using the `KeyType.NATURAL` `FindOption` passed to `find()` or `findMultiple()`.
117119

118120
[NOTE]
119121
====
120-
If the entity does not define a natural id, or if the natural id is not of a "simple" type, an exception will be thrown there.
122+
Hibernate historically offered the dedicated `byNaturalId()`, `bySimpleNaturalId()` and `byMultipleNaturalId()` APIs for loading one or more entities by natural id using its legacy "load access" approach. However, with JPA 3.2 and the introduction of `FindOption`, etc., these "load access" approaches are considered deprecated and are not discussed here.
121123
====
122124

123-
[[naturalid-mutability-caching]]
124-
==== Natural Id - Mutability and Caching
125-
126-
A natural id may be mutable or immutable. By default the `@NaturalId` annotation marks an immutable natural id attribute.
127-
An immutable natural id is expected to never change its value.
128-
129-
If the value(s) of the natural id attribute(s) change, `@NaturalId(mutable = true)` should be used instead.
130125

131-
[[naturalid-mutable-mapping-example]]
132-
.Mutable natural id mapping
126+
[[load-by-natural-id-example]]
127+
.Loading by natural id
133128
====
134129
[source,java]
135130
----
136-
include::{example-dir-naturalid}/MutableNaturalIdTest.java[tags=naturalid-mutable-mapping-example,indent=0]
131+
include::{example-dir-naturalid}/SimpleNaturalIdTest.java[tags=naturalid-loading-example,indent=0]
137132
----
138133
====
139134

140-
Within the Session, Hibernate maintains a mapping from natural id values to entity identifiers (PK) values.
141-
If natural ids values changed, it is possible for this mapping to become out of date until a flush occurs.
135+
When loading by natural id, the type of value accepted depends on the definition of the natural id.
142136

143-
To work around this condition, Hibernate will attempt to discover any such pending changes and adjust them when the `load()` or `getReference()` methods are executed.
144-
To be clear: this is only pertinent for mutable natural ids.
137+
* For single-attribute natural ids, whether defined by a basic or embedded type, the attribute type should be used.
138+
* For multi-attribute natural ids, Hibernate will accept a number of forms:
139+
140+
** If a link:{doc-javadoc-url}org/hibernate/annotations/NaturalIdClass.html[`@NaturalIdClass`] is defined, an instance of the natural id class may be used.
141+
** A `List` of the individual attribute values, ordered alphabetically by name, may be used.
142+
** A `Map` of the individual attribute values, keyed by the attribute name, may be used.
143+
144+
There are a few differences to be aware of when loading by natural id compared to loading by primary key. Most importantly, if the natural id is mutable and its values have changed, it is possible for the resolution caching to become out of date until a flush occurs resulting in incorrect results.
145+
To work around this condition, Hibernate will attempt to discover any such pending changes and adjust them prior to performing the load.
145146

146147
[IMPORTANT]
147148
====
148-
This _discovery and adjustment_ have a performance impact.
149-
If you are certain that none of the mutable natural ids already associated with the current `Session` have changed, you can disable this checking by calling `setSynchronizationEnabled(false)` (the default is `true`).
150-
This will force Hibernate to circumvent the checking of mutable natural ids.
149+
This _discovery and adjustment_ (synchronization) has a performance impact.
150+
If you are certain that none of the mutable natural ids already associated with the current `Session` have changed, you can disable this using the `NaturalIdSynchronization.DISABLED` option which will force Hibernate to skip the checking of mutable natural ids.
151+
To be clear: this is only pertinent for mutable natural ids.
151152
====
152153

153154
[[naturalid-mutable-synchronized-example]]
@@ -159,13 +160,3 @@ include::{example-dir-naturalid}/MutableNaturalIdTest.java[tags=naturalid-mutabl
159160
----
160161
====
161162

162-
Not only can this NaturalId-to-PK resolution be cached in the Session, but we can also have it cached in the second-level cache if second level caching is enabled.
163-
164-
[[naturalid-caching]]
165-
.Natural id caching
166-
====
167-
[source,java]
168-
----
169-
include::{example-dir-caching}/CacheableNaturalIdTest.java[tags=naturalid-cacheable-mapping-example,indent=0]
170-
----
171-
====

hibernate-core/src/main/java/org/hibernate/KeyType.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public enum KeyType implements FindOption {
1919
/// @see jakarta.persistence.Id
2020
/// @see jakarta.persistence.EmbeddedId
2121
/// @see jakarta.persistence.IdClass
22-
ID,
22+
IDENTIFIER,
2323

2424
/// Indicates to find based on the entity's natural-id, if one.
2525
///
@@ -28,5 +28,5 @@ public enum KeyType implements FindOption {
2828
///
2929
/// @implSpec Will trigger an [IllegalArgumentException] if the entity does
3030
/// not define a natural-id.
31-
NATURAL_ID
31+
NATURAL
3232
}

hibernate-core/src/main/java/org/hibernate/NaturalIdSynchronization.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import jakarta.persistence.FindOption;
88

99
/// Indicates whether to perform synchronization (a sort of flush)
10-
/// before a [find by natural-id][KeyType#NATURAL_ID].
10+
/// before a [find by natural-id][KeyType#NATURAL].
1111
///
1212
/// @author Steve Ebersole
1313
public enum NaturalIdSynchronization implements FindOption {

hibernate-core/src/main/java/org/hibernate/Session.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -476,7 +476,7 @@ public interface Session extends SharedSessionContract, EntityManager {
476476
///
477477
/// @implNote Note that Hibernate's implementation of this method can
478478
/// also be used for loading an entity by its [natural-id][org.hibernate.annotations.NaturalId]
479-
/// by passing [KeyType#NATURAL_ID] as a [FindOption] and the natural-id value as the `key` to load.
479+
/// by passing [KeyType#NATURAL] as a [FindOption] and the natural-id value as the `key` to load.
480480
@Override
481481
<T> T find(Class<T> entityClass, Object key, FindOption... options);
482482

@@ -1067,7 +1067,7 @@ public interface Session extends SharedSessionContract, EntityManager {
10671067
/// @throws HibernateException If the given class does not resolve as a mapped entity,
10681068
/// or if the entity does not declare a natural id
10691069
///
1070-
/// @deprecated (since 7.3) : Use {@linkplain #find} with [KeyType#NATURAL_ID] instead.
1070+
/// @deprecated (since 7.3) : Use {@linkplain #find} with [KeyType#NATURAL] instead.
10711071
@Deprecated
10721072
<T> NaturalIdLoadAccess<T> byNaturalId(Class<T> entityClass);
10731073

@@ -1083,7 +1083,7 @@ public interface Session extends SharedSessionContract, EntityManager {
10831083
/// @throws HibernateException If the given name does not resolve to a mapped entity,
10841084
/// or if the entity does not declare a natural id
10851085
///
1086-
/// @deprecated (since 7.3) : Use {@linkplain #find} with [KeyType#NATURAL_ID] instead.
1086+
/// @deprecated (since 7.3) : Use {@linkplain #find} with [KeyType#NATURAL] instead.
10871087
@Deprecated
10881088
<T> NaturalIdLoadAccess<T> byNaturalId(String entityName);
10891089

@@ -1099,7 +1099,7 @@ public interface Session extends SharedSessionContract, EntityManager {
10991099
/// @throws HibernateException If the given class does not resolve as a mapped entity,
11001100
/// or if the entity does not declare a natural id
11011101
///
1102-
/// @deprecated (since 7.3) : Use {@linkplain #find} with [KeyType#NATURAL_ID] instead.
1102+
/// @deprecated (since 7.3) : Use {@linkplain #find} with [KeyType#NATURAL] instead.
11031103
@Deprecated
11041104
<T> SimpleNaturalIdLoadAccess<T> bySimpleNaturalId(Class<T> entityClass);
11051105

@@ -1115,7 +1115,7 @@ public interface Session extends SharedSessionContract, EntityManager {
11151115
/// @throws HibernateException If the given name does not resolve to a mapped entity,
11161116
/// or if the entity does not declare a natural id
11171117
///
1118-
/// @deprecated (since 7.3) : Use {@linkplain #find} with [KeyType#NATURAL_ID] instead.
1118+
/// @deprecated (since 7.3) : Use {@linkplain #find} with [KeyType#NATURAL] instead.
11191119
@Deprecated
11201120
<T> SimpleNaturalIdLoadAccess<T> bySimpleNaturalId(String entityName);
11211121

@@ -1130,7 +1130,7 @@ public interface Session extends SharedSessionContract, EntityManager {
11301130
/// @throws HibernateException If the given class does not resolve as a mapped entity,
11311131
/// or if the entity does not declare a natural id
11321132
///
1133-
/// @deprecated (since 7.3) : Use {@linkplain #findMultiple} with [KeyType#NATURAL_ID] instead.
1133+
/// @deprecated (since 7.3) : Use {@linkplain #findMultiple} with [KeyType#NATURAL] instead.
11341134
///
11351135
@Deprecated
11361136
<T> NaturalIdMultiLoadAccess<T> byMultipleNaturalId(Class<T> entityClass);
@@ -1146,7 +1146,7 @@ public interface Session extends SharedSessionContract, EntityManager {
11461146
/// @throws HibernateException If the given name does not resolve to a mapped entity,
11471147
/// or if the entity does not declare a natural id
11481148
///
1149-
/// @deprecated (since 7.3) : Use {@linkplain #findMultiple} with [KeyType#NATURAL_ID] instead.
1149+
/// @deprecated (since 7.3) : Use {@linkplain #findMultiple} with [KeyType#NATURAL] instead.
11501150
@Deprecated
11511151
<T> NaturalIdMultiLoadAccess<T> byMultipleNaturalId(String entityName);
11521152

hibernate-core/src/main/java/org/hibernate/internal/MultiIdentifierLoadAccessImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ public <K> List<T> multiLoad(K... ids) {
178178
private FindMultipleByKeyOperation<T> buildOperation() {
179179
return new FindMultipleByKeyOperation<T>(
180180
entityPersister,
181-
KeyType.ID,
181+
KeyType.IDENTIFIER,
182182
batchSize,
183183
sessionCheckMode,
184184
removalsMode,

hibernate-core/src/main/java/org/hibernate/internal/NaturalIdHelper.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public static void performAnyNeededCrossReferenceSynchronizations(
4343
// first check if synchronization (this process) was disabled
4444
if ( synchronizationEnabled
4545
// only mutable natural-ids need this processing
46-
&& entityMappingType.getNaturalIdMapping().isMutable()
46+
&& entityMappingType.requireNaturalIdMapping().isMutable()
4747
// skip synchronization when not in a transaction
4848
&& session.isTransactionInProgress() ) {
4949
final var persister = entityMappingType.getEntityPersister();

hibernate-core/src/main/java/org/hibernate/internal/NaturalIdMultiLoadAccessStandard.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ public List<T> multiLoad(List<?> ids) {
141141
private FindMultipleByKeyOperation<T> buildOperation() {
142142
return new FindMultipleByKeyOperation<T>(
143143
entityDescriptor,
144-
KeyType.NATURAL_ID,
144+
KeyType.NATURAL,
145145
batchSize == null ? null : new BatchSize( batchSize ),
146146
SessionCheckMode.ENABLED,
147147
removalsMode,

hibernate-core/src/main/java/org/hibernate/internal/find/FindByKeyOperation.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
import static org.hibernate.jpa.SpecHints.HINT_SPEC_LOCK_TIMEOUT;
4848
import static org.hibernate.proxy.HibernateProxy.extractLazyInitializer;
4949

50-
/// Support for loading a single entity by key (either [id][KeyType#ID] or [natural-id][KeyType#NATURAL_ID]).
50+
/// Support for loading a single entity by key (either [id][KeyType#IDENTIFIER] or [natural-id][KeyType#NATURAL]).
5151
///
5252
/// @see org.hibernate.Session#find
5353
/// @see KeyType
@@ -56,7 +56,7 @@
5656
public class FindByKeyOperation<T> implements NaturalIdLoader.Options {
5757
private final EntityPersister entityDescriptor;
5858

59-
private KeyType keyType = KeyType.ID;
59+
private KeyType keyType = KeyType.IDENTIFIER;
6060

6161
private CacheStoreMode cacheStoreMode;
6262
private CacheRetrieveMode cacheRetrieveMode;
@@ -164,7 +164,7 @@ private void enabledFetchProfile(String profileName) {
164164
}
165165

166166
public T performFind(Object key, LoadAccessContext loadAccessContext) {
167-
if ( keyType == KeyType.NATURAL_ID ) {
167+
if ( keyType == KeyType.NATURAL ) {
168168
return findByNaturalId( key, loadAccessContext );
169169
}
170170
else {
@@ -174,15 +174,15 @@ public T performFind(Object key, LoadAccessContext loadAccessContext) {
174174
}
175175

176176
private T findByNaturalId(Object key, LoadAccessContext loadAccessContext) {
177+
final NaturalIdMapping naturalIdMapping = entityDescriptor.requireNaturalIdMapping();
177178
final SessionImplementor session = loadAccessContext.getSession();
178179

179180
performAnyNeededCrossReferenceSynchronizations(
180-
naturalIdSynchronization == NaturalIdSynchronization.ENABLED,
181+
naturalIdSynchronization != NaturalIdSynchronization.DISABLED,
181182
entityDescriptor,
182183
session
183184
);
184185

185-
final NaturalIdMapping naturalIdMapping = entityDescriptor.getNaturalIdMapping();
186186
final var normalizedKey = naturalIdMapping.normalizeInput( key );
187187

188188
final Object cachedResolution = getCachedNaturalIdResolution( normalizedKey, loadAccessContext );

0 commit comments

Comments
 (0)