Skip to content

Commit 2659cfb

Browse files
committed
HHH-19989 introduce RemovalsMode.EXCLUDE
1 parent d4bfb31 commit 2659cfb

File tree

8 files changed

+243
-56
lines changed

8 files changed

+243
-56
lines changed

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,15 @@ public enum RemovalsMode implements FindMultipleOption {
3131
/**
3232
* The default. Removed entities are replaced with {@code null} in the load result.
3333
*/
34-
REPLACE
34+
REPLACE,
35+
/**
36+
* Removed entities are excluded from the load result.
37+
* <p>
38+
* This option is incompatible with {@link OrderingMode#ORDERED}.
39+
* It must be used in conjunction with {@link OrderingMode#UNORDERED}
40+
* and {@link SessionCheckMode#ENABLED}.
41+
*
42+
* @since 7.3
43+
*/
44+
EXCLUDE
3545
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
* matching the identifiers to be loaded - <ul>
1616
* <li>Entities which are in a managed state are not reloaded from the database.
1717
* Those identifiers are removed from the SQL restriction sent to the database.
18-
* <li>Entities which are in a removed state are {@linkplain RemovalsMode#REPLACE excluded}
18+
* <li>Entities which are in a removed state are {@linkplain RemovalsMode#REPLACE replaced with null}
1919
* from the result by default, but can be {@linkplain RemovalsMode#INCLUDE included} if desired.
2020
* </ul>
2121
* <p/>

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,6 @@ public void with(OrderingMode orderingMode) {
126126
}
127127

128128
@Override
129-
@SuppressWarnings( "unchecked" )
130129
public List<T> multiLoad(Object... ids) {
131130
return buildOperation()
132131
.performFind( List.of( ids ), graphSemantic, rootGraph, (LoadAccessContext) session );
@@ -135,7 +134,7 @@ public List<T> multiLoad(Object... ids) {
135134
@Override
136135
public List<T> multiLoad(List<?> ids) {
137136
return buildOperation()
138-
.performFind( (List<Object>) ids, graphSemantic, rootGraph, (LoadAccessContext) session );
137+
.performFind( ids, graphSemantic, rootGraph, (LoadAccessContext) session );
139138
}
140139

141140
private FindMultipleByKeyOperation<T> buildOperation() {

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

Lines changed: 35 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
import org.hibernate.loader.ast.spi.MultiIdLoadOptions;
3434
import org.hibernate.loader.ast.spi.MultiNaturalIdLoadOptions;
3535
import org.hibernate.loader.internal.LoadAccessContext;
36-
import org.hibernate.metamodel.mapping.NaturalIdMapping;
3736
import org.hibernate.persister.entity.EntityPersister;
3837

3938
import java.util.HashSet;
@@ -170,23 +169,20 @@ private void enabledFetchProfile(String profileName) {
170169
}
171170

172171
public List<T> performFind(
173-
List<Object> keys,
172+
List<?> keys,
174173
@Nullable GraphSemantic graphSemantic,
175174
@Nullable RootGraphImplementor<T> rootGraph,
176175
LoadAccessContext loadAccessContext) {
177176
// todo (natural-id-class) : these impls are temporary
178177
// longer term, move the logic here as much of it can be shared
179-
if ( keyType == KeyType.NATURAL ) {
180-
return findByNaturalIds( keys, graphSemantic, rootGraph, loadAccessContext );
181-
}
182-
else {
183-
return findByIds( keys, graphSemantic, rootGraph, loadAccessContext );
184-
}
178+
return keyType == KeyType.NATURAL
179+
? findByNaturalIds( keys, graphSemantic, rootGraph, loadAccessContext )
180+
: findByIds( keys, graphSemantic, rootGraph, loadAccessContext );
185181
}
186182

187-
private List<T> findByNaturalIds(List<Object> keys, GraphSemantic graphSemantic, RootGraphImplementor<T> rootGraph, LoadAccessContext loadAccessContext) {
188-
final NaturalIdMapping naturalIdMapping = entityDescriptor.requireNaturalIdMapping();
189-
final SessionImplementor session = loadAccessContext.getSession();
183+
private List<T> findByNaturalIds(List<?> keys, GraphSemantic graphSemantic, RootGraphImplementor<T> rootGraph, LoadAccessContext loadAccessContext) {
184+
final var naturalIdMapping = entityDescriptor.requireNaturalIdMapping();
185+
final var session = loadAccessContext.getSession();
190186

191187
performAnyNeededCrossReferenceSynchronizations(
192188
naturalIdSynchronization != NaturalIdSynchronization.DISABLED,
@@ -197,15 +193,15 @@ private List<T> findByNaturalIds(List<Object> keys, GraphSemantic graphSemantic,
197193
return withOptions( loadAccessContext, graphSemantic, rootGraph, () -> {
198194
// normalize the incoming natural-id values and get them in array form as needed
199195
// by MultiNaturalIdLoader
200-
final Object[] naturalIds = new Object[keys.size()];
201-
for ( int i = 0; i < keys.size(); i++ ) {
202-
final Object key = keys.get( i );
203-
naturalIds[i] = naturalIdMapping.normalizeInput( key );
196+
final int size = keys.size();
197+
final var naturalIds = new Object[size];
198+
for ( int i = 0; i < size; i++ ) {
199+
naturalIds[i] = naturalIdMapping.normalizeInput( keys.get( i ) );
204200
}
205-
206201
//noinspection unchecked
207-
return (List<T>)entityDescriptor.getMultiNaturalIdLoader()
208-
.multiLoad( naturalIds, this, session );
202+
return (List<T>)
203+
entityDescriptor.getMultiNaturalIdLoader()
204+
.multiLoad( naturalIds, this, session );
209205
} );
210206
}
211207

@@ -216,10 +212,12 @@ private List<T> withOptions(
216212
Supplier<List<T>> action) {
217213
final var session = loadAccessContext.getSession();
218214
final var influencers = session.getLoadQueryInfluencers();
219-
final var fetchProfiles = influencers.adjustFetchProfiles( disabledFetchProfiles, enabledFetchProfiles );
220-
final var effectiveEntityGraph = rootGraph == null
221-
? null
222-
: influencers.applyEntityGraph( rootGraph, graphSemantic );
215+
final var fetchProfiles =
216+
influencers.adjustFetchProfiles( disabledFetchProfiles, enabledFetchProfiles );
217+
final var effectiveEntityGraph =
218+
rootGraph == null
219+
? null
220+
: influencers.applyEntityGraph( rootGraph, graphSemantic );
223221

224222
final var readOnly = session.isDefaultReadOnly();
225223
session.setDefaultReadOnly( readOnlyMode == ReadOnlyMode.READ_ONLY );
@@ -241,21 +239,21 @@ private List<T> withOptions(
241239
}
242240
}
243241

244-
private Object getCachedNaturalIdResolution(
245-
Object normalizedNaturalIdValue,
246-
LoadAccessContext loadAccessContext) {
247-
loadAccessContext.checkOpenOrWaitingForAutoClose();
248-
loadAccessContext.pulseTransactionCoordinator();
249-
250-
return loadAccessContext
251-
.getSession()
252-
.getPersistenceContextInternal()
253-
.getNaturalIdResolutions()
254-
.findCachedIdByNaturalId( normalizedNaturalIdValue, entityDescriptor );
255-
}
256-
257-
private List<T> findByIds(List<Object> keys, GraphSemantic graphSemantic, RootGraphImplementor<T> rootGraph, LoadAccessContext loadAccessContext) {
258-
final Object[] ids = keys.toArray( new Object[0] );
242+
// private Object getCachedNaturalIdResolution(
243+
// Object normalizedNaturalIdValue,
244+
// LoadAccessContext loadAccessContext) {
245+
// loadAccessContext.checkOpenOrWaitingForAutoClose();
246+
// loadAccessContext.pulseTransactionCoordinator();
247+
//
248+
// return loadAccessContext
249+
// .getSession()
250+
// .getPersistenceContextInternal()
251+
// .getNaturalIdResolutions()
252+
// .findCachedIdByNaturalId( normalizedNaturalIdValue, entityDescriptor );
253+
// }
254+
255+
private List<T> findByIds(List<?> keys, GraphSemantic graphSemantic, RootGraphImplementor<T> rootGraph, LoadAccessContext loadAccessContext) {
256+
final var ids = keys.toArray( new Object[0] );
259257
//noinspection unchecked
260258
return withOptions( loadAccessContext, graphSemantic, rootGraph,
261259
() -> (List<T>) entityDescriptor.multiLoad( ids, loadAccessContext.getSession(), this ) );

hibernate-core/src/main/java/org/hibernate/loader/ast/internal/AbstractMultiIdEntityLoader.java

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ private boolean loadFromEnabledCaches(
212212
EntityKey entityKey,
213213
List<Object> result,
214214
int i) {
215-
return (loadOptions.getSessionCheckMode() == SessionCheckMode.ENABLED
215+
return ( loadOptions.getSessionCheckMode() == SessionCheckMode.ENABLED
216216
|| loadOptions.isSecondLevelCacheCheckingEnabled() )
217217
&& isLoadFromCaches( loadOptions, entityKey, lockOptions, result, i, session );
218218
}
@@ -223,15 +223,19 @@ private boolean isLoadFromCaches(
223223
LockOptions lockOptions,
224224
List<Object> results, int i,
225225
SharedSessionContractImplementor session) {
226-
227226
if ( loadOptions.getSessionCheckMode() == SessionCheckMode.ENABLED ) {
227+
final var removalsMode = loadOptions.getRemovalsMode();
228+
if ( removalsMode == RemovalsMode.EXCLUDE ) {
229+
// note, this method is only called from orderedMultiLoad()
230+
throw new IllegalArgumentException( "RemovalsMode.EXCLUDE is incompatible with OrderingMode.ORDERED" );
231+
}
228232
// look for it in the Session first
229233
final var entry = loadFromSessionCache( entityKey, lockOptions, GET, session );
230234
final Object entity = entry.entity();
231235
if ( entity != null ) {
232236
// put a null in the results
233237
final Object result;
234-
if ( loadOptions.getRemovalsMode() == RemovalsMode.INCLUDE
238+
if ( removalsMode == RemovalsMode.INCLUDE
235239
|| entry.isManaged() ) {
236240
result = entity;
237241
}
@@ -377,13 +381,16 @@ private <R> List<Object> loadFromCaches(
377381
// look for it in the Session first
378382
final var entry = loadFromSessionCache( entityKey, lockOptions, GET, session );
379383
final Object sessionEntity;
380-
if ( loadOptions.getSessionCheckMode() == SessionCheckMode.ENABLED ) {
384+
if ( loadOptions.getSessionCheckMode() == SessionCheckMode.ENABLED ) {
381385
sessionEntity = entry.entity();
382-
if ( sessionEntity != null
383-
&& loadOptions.getRemovalsMode() == RemovalsMode.REPLACE
384-
&& !entry.isManaged() ) {
385-
resolutionConsumer.consume( i, entityKey, null );
386-
return unresolvedIds;
386+
if ( sessionEntity != null && !entry.isManaged() ) {
387+
switch ( loadOptions.getRemovalsMode() ) {
388+
case REPLACE :
389+
resolutionConsumer.consume( i, entityKey, null );
390+
return unresolvedIds;
391+
case EXCLUDE:
392+
return unresolvedIds;
393+
}
387394
}
388395
}
389396
else {

hibernate-core/src/main/java/org/hibernate/loader/ast/internal/AbstractMultiNaturalIdLoader.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ private <K> List<E> sortResults(
129129
return results;
130130
}
131131

132-
private <K> Object entityForNaturalId(PersistenceContext context, K naturalId) {
132+
private Object entityForNaturalId(PersistenceContext context, Object naturalId) {
133133
final var descriptor = getEntityDescriptor();
134134
final Object id = context.getNaturalIdResolutions().findCachedIdByNaturalId( naturalId, descriptor );
135135
// id can be null if a non-existent natural id is requested, or a mutable natural id was changed and then deleted
@@ -142,19 +142,26 @@ private <K> Object[] checkPersistenceContextForCachedResults(
142142
SharedSessionContractImplementor session,
143143
LockOptions lockOptions,
144144
Consumer<E> results ) {
145+
final var removalsMode = loadOptions.getRemovalsMode();
146+
if ( removalsMode == RemovalsMode.EXCLUDE
147+
&& loadOptions.getOrderingMode() == OrderingMode.ORDERED ) {
148+
throw new IllegalArgumentException( "RemovalsMode.EXCLUDE is incompatible with OrderingMode.ORDERED" );
149+
}
145150
final List<K> unresolvedIds = arrayList( naturalIds.length );
146151
final var context = session.getPersistenceContextInternal();
147152
for ( K naturalId : naturalIds ) {
148153
final Object entity = entityForNaturalId( context, naturalId );
149154
if ( entity != null ) {
150155
// Entity is already in the persistence context
151156
final var entry = context.getEntry( entity );
152-
if ( loadOptions.getRemovalsMode() == RemovalsMode.INCLUDE
157+
if ( removalsMode == RemovalsMode.INCLUDE
153158
|| !entry.getStatus().isDeletedOrGone() ) {
154159
// either a managed entry, or a deleted one with returnDeleted enabled
155160
upgradeLock( entity, entry, lockOptions, session );
156-
final Object result = context.proxyFor( entity );
157-
results.accept( (E) result );
161+
if ( removalsMode != RemovalsMode.EXCLUDE ) {
162+
final Object result = context.proxyFor( entity );
163+
results.accept( (E) result );
164+
}
158165
}
159166
}
160167
else {

hibernate-core/src/test/java/org/hibernate/orm/test/mapping/naturalid/LoadByNaturalIdTest.java

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111
import jakarta.persistence.Timeout;
1212
import org.hibernate.KeyType;
1313
import org.hibernate.LockMode;
14+
import org.hibernate.OrderingMode;
1415
import org.hibernate.ReadOnlyMode;
16+
import org.hibernate.RemovalsMode;
17+
import org.hibernate.SessionCheckMode;
1518
import org.hibernate.annotations.NaturalId;
1619
import org.hibernate.engine.spi.SessionImplementor;
1720
import org.hibernate.loader.ast.spi.NaturalIdLoader;
@@ -20,7 +23,7 @@
2023
import org.hibernate.testing.orm.junit.JiraKey;
2124
import org.hibernate.testing.orm.junit.SessionFactory;
2225
import org.hibernate.testing.orm.junit.SessionFactoryScope;
23-
import org.junit.jupiter.api.BeforeAll;
26+
import org.junit.jupiter.api.BeforeEach;
2427
import org.junit.jupiter.api.Test;
2528

2629
import jakarta.persistence.CascadeType;
@@ -31,6 +34,9 @@
3134
import jakarta.persistence.OneToMany;
3235

3336
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
37+
import static org.junit.jupiter.api.Assertions.assertEquals;
38+
import static org.junit.jupiter.api.Assertions.assertNotNull;
39+
import static org.junit.jupiter.api.Assertions.assertNull;
3440

3541
@DomainModel(
3642
annotatedClasses = {
@@ -47,8 +53,9 @@ public class LoadByNaturalIdTest {
4753
private static final Long CHILD_ID = 1l;
4854
private static final String CHILD_NAME = "Filippo";
4955

50-
@BeforeAll
56+
@BeforeEach
5157
public void setUp(SessionFactoryScope scope) {
58+
scope.getSessionFactory().getSchemaManager().truncate();
5259
scope.inTransaction(
5360
session -> {
5461
Parent parent = new Parent( PARENT_ID, PARENT_NAME );
@@ -119,6 +126,62 @@ void testFindOptions(SessionFactoryScope factoryScope) {
119126
} );
120127
}
121128

129+
@Test
130+
void testFindMultipleOptions(SessionFactoryScope factoryScope) {
131+
factoryScope.inTransaction( (session) -> {
132+
var multiple = session.findMultiple(
133+
Parent.class,
134+
List.of( "Luigi", "Filippo" ),
135+
KeyType.NATURAL,
136+
SessionCheckMode.ENABLED,
137+
OrderingMode.UNORDERED,
138+
RemovalsMode.EXCLUDE
139+
);
140+
assertEquals( 1, multiple.size() );
141+
} );
142+
factoryScope.inTransaction( (session) -> {
143+
session.remove( session.find( Parent.class, "Luigi", KeyType.NATURAL ) );
144+
var multiple = session.findMultiple(
145+
Parent.class,
146+
List.of( "Luigi", "Filippo" ),
147+
KeyType.NATURAL,
148+
SessionCheckMode.ENABLED,
149+
OrderingMode.UNORDERED,
150+
RemovalsMode.EXCLUDE
151+
);
152+
assertEquals( 0, multiple.size() );
153+
} );
154+
}
155+
156+
@Test
157+
void testFindMultipleOptions2(SessionFactoryScope factoryScope) {
158+
factoryScope.inTransaction( (session) -> {
159+
var multiple = session.findMultiple(
160+
Parent.class,
161+
List.of( "Luigi", "Filippo" ),
162+
KeyType.NATURAL,
163+
SessionCheckMode.ENABLED,
164+
RemovalsMode.REPLACE
165+
);
166+
assertEquals( 2, multiple.size() );
167+
assertNotNull( multiple.get(0) );
168+
assertNull( multiple.get(1) );
169+
} );
170+
factoryScope.inTransaction( (session) -> {
171+
session.remove( session.find( Parent.class, "Luigi", KeyType.NATURAL ) );
172+
var multiple = session.findMultiple(
173+
Parent.class,
174+
List.of( "Luigi", "Filippo" ),
175+
KeyType.NATURAL,
176+
SessionCheckMode.ENABLED,
177+
RemovalsMode.REPLACE
178+
);
179+
assertEquals( 2, multiple.size() );
180+
assertNull( multiple.get(0) );
181+
assertNull( multiple.get(1) );
182+
} );
183+
}
184+
122185
private static NaturalIdLoader<?> getNaturalIdLoader(Class clazz, SessionImplementor session) {
123186
return session.getFactory()
124187
.getRuntimeMetamodels()

0 commit comments

Comments
 (0)