From 0530f020706f80b643f03987704048a885fac941 Mon Sep 17 00:00:00 2001 From: Jan Schatteman Date: Fri, 3 Oct 2025 23:28:54 +0200 Subject: [PATCH 1/3] HHH-19829 - Deprecate MultiIdentifierLoadAccess and the Session.byMultipleIds() methods Signed-off-by: Jan Schatteman --- .../java/org/hibernate/IncludeRemovals.java | 31 + .../java/org/hibernate/MultiFindOption.java | 14 + .../hibernate/MultiIdentifierLoadAccess.java | 4 + .../java/org/hibernate/OrderedReturn.java | 32 + .../src/main/java/org/hibernate/Session.java | 10 + .../java/org/hibernate/SessionChecking.java | 30 + .../org/hibernate/internal/SessionImpl.java | 12 + .../test/dynamicmap/FindOperationTests.java | 13 + .../test/loading/multiLoad/MultiLoadTest.java | 592 ++++++++++++------ 9 files changed, 561 insertions(+), 177 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/IncludeRemovals.java create mode 100644 hibernate-core/src/main/java/org/hibernate/MultiFindOption.java create mode 100644 hibernate-core/src/main/java/org/hibernate/OrderedReturn.java create mode 100644 hibernate-core/src/main/java/org/hibernate/SessionChecking.java diff --git a/hibernate-core/src/main/java/org/hibernate/IncludeRemovals.java b/hibernate-core/src/main/java/org/hibernate/IncludeRemovals.java new file mode 100644 index 000000000000..243225a1a3e5 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/IncludeRemovals.java @@ -0,0 +1,31 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate; + + +import jakarta.persistence.EntityGraph; +import jakarta.persistence.FindOption; + +import java.util.List; + +/** + * MultiFindOption implementation to specify whether the returned list + * of entity instances should contain instances that have been + * {@linkplain Session#remove(Object) marked for removal} in the + * current session, but not yet deleted in the database. + *

+ * The default is {@link #EXCLUDE}, meaning that instances marked for + * removal are replaced by null in the returned list of entities when {@link OrderedReturn} + * is used. + * + * @see org.hibernate.MultiFindOption + * @see OrderedReturn + * @see org.hibernate.Session#findMultiple(Class, List, FindOption...) + * @see org.hibernate.Session#findMultiple(EntityGraph, List , FindOption...) + */ +public enum IncludeRemovals implements MultiFindOption { + INCLUDE, + EXCLUDE +} diff --git a/hibernate-core/src/main/java/org/hibernate/MultiFindOption.java b/hibernate-core/src/main/java/org/hibernate/MultiFindOption.java new file mode 100644 index 000000000000..400f7b9354ad --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/MultiFindOption.java @@ -0,0 +1,14 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate; + + +import jakarta.persistence.FindOption; + +/** + * Simple marker interface for FindOptions which can be applied to multiple id loading. + */ +public interface MultiFindOption extends FindOption { +} diff --git a/hibernate-core/src/main/java/org/hibernate/MultiIdentifierLoadAccess.java b/hibernate-core/src/main/java/org/hibernate/MultiIdentifierLoadAccess.java index 0d33cbff22db..2b5f850ff4b3 100644 --- a/hibernate-core/src/main/java/org/hibernate/MultiIdentifierLoadAccess.java +++ b/hibernate-core/src/main/java/org/hibernate/MultiIdentifierLoadAccess.java @@ -31,7 +31,11 @@ * @see Session#byMultipleIds(Class) * * @author Steve Ebersole + + * @deprecated Use forms of {@linkplain Session#findMultiple} accepting + * {@linkplain jakarta.persistence.FindOption} instead of {@linkplain Session#byMultipleIds}. */ +@Deprecated(since = "7.2", forRemoval = true) public interface MultiIdentifierLoadAccess { /** diff --git a/hibernate-core/src/main/java/org/hibernate/OrderedReturn.java b/hibernate-core/src/main/java/org/hibernate/OrderedReturn.java new file mode 100644 index 000000000000..5ba775c590ab --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/OrderedReturn.java @@ -0,0 +1,32 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate; + + +import jakarta.persistence.EntityGraph; +import jakarta.persistence.FindOption; + +import java.util.List; + +/** + * MultiFindOption implementation to specify whether the returned list + * of entity instances should be ordered, where the position of an entity + * instance is determined by the position of its identifier + * in the list of ids passed to {@code findMultiple(...)}. + *

+ * The default is {@link #ORDERED}, meaning the positions of the entities + * in the returned list correspond to the positions of their ids. In this case, + * the {@link IncludeRemovals} handling of entities marked for removal + * becomes important. + * + * @see org.hibernate.MultiFindOption + * @see IncludeRemovals + * @see org.hibernate.Session#findMultiple(Class, List, FindOption...) + * @see org.hibernate.Session#findMultiple(EntityGraph, List , FindOption...) + */ +public enum OrderedReturn implements MultiFindOption { + ORDERED, + UNORDERED +} diff --git a/hibernate-core/src/main/java/org/hibernate/Session.java b/hibernate-core/src/main/java/org/hibernate/Session.java index abc2c2f3eec6..8d925c331240 100644 --- a/hibernate-core/src/main/java/org/hibernate/Session.java +++ b/hibernate-core/src/main/java/org/hibernate/Session.java @@ -1165,7 +1165,12 @@ public interface Session extends SharedSessionContract, EntityManager { * @throws HibernateException If the given class does not resolve as a mapped entity * * @see #findMultiple(Class, List, FindOption...) + * + * @deprecated This method will be removed. + * Use {@link #findMultiple(Class, List, FindOption...)} instead. + * See {@link MultiFindOption}. */ + @Deprecated(since = "7.2", forRemoval = true) MultiIdentifierLoadAccess byMultipleIds(Class entityClass); /** @@ -1177,7 +1182,12 @@ public interface Session extends SharedSessionContract, EntityManager { * @return an instance of {@link MultiIdentifierLoadAccess} for executing the lookup * * @throws HibernateException If the given name does not resolve to a mapped entity + * + * @deprecated This method will be removed. + * Use {@link #findMultiple(Class, List, FindOption...)} instead. + * See {@link MultiFindOption}. */ + @Deprecated(since = "7.2", forRemoval = true) MultiIdentifierLoadAccess byMultipleIds(String entityName); /** diff --git a/hibernate-core/src/main/java/org/hibernate/SessionChecking.java b/hibernate-core/src/main/java/org/hibernate/SessionChecking.java new file mode 100644 index 000000000000..37757af3655d --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/SessionChecking.java @@ -0,0 +1,30 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate; + + +import jakarta.persistence.EntityGraph; +import jakarta.persistence.FindOption; + +import java.util.List; + +/** + * MultiFindOption implementation to specify whether the ids of managed entity instances already + * cached in the current persistence context should be excluded. + * from the list of ids sent to the database. + *

+ * The default is {@link #DISABLED}, meaning all ids are included and sent to the database. + * + * Use {@link #ENABLED} to exclude already managed entity instance ids from + * the list of ids sent to the database. + * + * @see org.hibernate.MultiFindOption + * @see org.hibernate.Session#findMultiple(Class, List , FindOption...) + * @see org.hibernate.Session#findMultiple(EntityGraph, List , FindOption...) + */ +public enum SessionChecking implements MultiFindOption { + ENABLED, + DISABLED +} diff --git a/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java b/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java index c48197058fd7..550560ee94f1 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java @@ -964,6 +964,15 @@ else if ( option instanceof ReadOnlyMode ) { else if ( option instanceof BatchSize batchSizeOption ) { batchSize = batchSizeOption.batchSize(); } + else if ( option instanceof SessionChecking sessionChecking ) { + loadAccess.enableSessionCheck( option == sessionChecking.ENABLED ); + } + else if ( option instanceof OrderedReturn orderedReturn ) { + loadAccess.enableOrderedReturn( option == orderedReturn.ORDERED ); + } + else if ( option instanceof IncludeRemovals includeRemovals ) { + loadAccess.enableReturnOfDeletedEntities( option == includeRemovals.INCLUDE ); + } } loadAccess.with( lockOptions ) .with( interpretCacheMode( storeMode, retrieveMode ) ) @@ -2291,6 +2300,9 @@ else if ( option instanceof EnabledFetchProfile enabledFetchProfile ) { else if ( option instanceof ReadOnlyMode ) { loadAccess.withReadOnly( option == ReadOnlyMode.READ_ONLY ); } + else if ( option instanceof MultiFindOption multiFindOption ) { + throw new IllegalArgumentException( "Option '" + multiFindOption + "' can only be used in 'findMultiple()'" ); + } } if ( lockOptions.getLockMode().isPessimistic() ) { if ( lockOptions.getTimeOut() == WAIT_FOREVER_MILLI ) { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/dynamicmap/FindOperationTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/dynamicmap/FindOperationTests.java index 4b7e77ec858e..d2b6069a8da8 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/dynamicmap/FindOperationTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/dynamicmap/FindOperationTests.java @@ -4,7 +4,10 @@ */ package org.hibernate.orm.test.dynamicmap; +import org.hibernate.IncludeRemovals; +import org.hibernate.OrderedReturn; import org.hibernate.ReadOnlyMode; +import org.hibernate.SessionChecking; import org.hibernate.graph.RootGraph; import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.SessionFactory; @@ -17,6 +20,7 @@ import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; /** * @author Steve Ebersole @@ -58,6 +62,15 @@ void testFindWithOptions(SessionFactoryScope factoryScope) { } ); } + @Test + void testFindWithIllegalOptions(SessionFactoryScope factoryScope) { + factoryScope.inTransaction( (session) -> { + assertThrows( IllegalArgumentException.class, () ->session.find( "artist", 1, SessionChecking.ENABLED ) ); + assertThrows( IllegalArgumentException.class, () ->session.find( "artist", 1, OrderedReturn.ORDERED ) ); + assertThrows( IllegalArgumentException.class, () ->session.find( "artist", 1, IncludeRemovals.INCLUDE ) ); + } ); + } + @Test void testFindWithGraph(SessionFactoryScope factoryScope) { factoryScope.inTransaction( (session) -> { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/loading/multiLoad/MultiLoadTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/loading/multiLoad/MultiLoadTest.java index d4174d6a13bf..fa143411e7b4 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/loading/multiLoad/MultiLoadTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/loading/multiLoad/MultiLoadTest.java @@ -4,11 +4,15 @@ */ package org.hibernate.orm.test.loading.multiLoad; +import java.util.ArrayList; import java.util.List; import java.util.Objects; import org.hibernate.CacheMode; import org.hibernate.Hibernate; +import org.hibernate.IncludeRemovals; +import org.hibernate.OrderedReturn; +import org.hibernate.SessionChecking; import org.hibernate.annotations.BatchSize; import org.hibernate.cache.spi.access.AccessType; import org.hibernate.cfg.AvailableSettings; @@ -37,13 +41,13 @@ import jakarta.persistence.Table; import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * @author Steve Ebersole @@ -86,7 +90,7 @@ public void testBasicMultiLoad(SessionFactoryScope scope) { final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); scope.inTransaction( session -> { - statementInspector.getSqlQueries().clear(); + statementInspector.clear(); List list = session.byMultipleIds( SimpleEntity.class ).multiLoad( ids( 5 ) ); assertEquals( 5, list.size() ); @@ -214,10 +218,18 @@ public void testDuplicatedRequestedIdswithDisableOrderedReturn(SessionFactorySco scope.inTransaction( session -> { // un-ordered multiLoad - List list = session.byMultipleIds( SimpleEntity.class ) - .enableOrderedReturn( false ) - .multiLoad( 1, 2, 3, 2, 2 ); - assertEquals( 3, list.size() ); + { + final List list = session.byMultipleIds( SimpleEntity.class ) + .enableOrderedReturn( false ) + .multiLoad( 1, 2, 3, 2, 2 ); + assertEquals( 3, list.size() ); + } + + { + final List list = session.findMultiple( SimpleEntity.class, List.of( 1, 2, 3, 2, 2 ), OrderedReturn.UNORDERED ); + assertEquals( 3, list.size() ); + + } } ); } @@ -228,13 +240,20 @@ public void testNonExistentIdRequest(SessionFactoryScope scope) { scope.inTransaction( session -> { // ordered multiLoad - List list = session.byMultipleIds( SimpleEntity.class ).multiLoad( 1, 699, 2 ); - assertEquals( 3, list.size() ); - assertNull( list.get( 1 ) ); + { + List list = session.byMultipleIds( SimpleEntity.class ).multiLoad( 1, 699, 2 ); + assertEquals( 3, list.size() ); + assertNull( list.get( 1 ) ); + + // un-ordered multiLoad + list = session.byMultipleIds( SimpleEntity.class ).enableOrderedReturn( false ).multiLoad( 1, 699, 2 ); + assertEquals( 2, list.size() ); + } - // un-ordered multiLoad - list = session.byMultipleIds( SimpleEntity.class ).enableOrderedReturn( false ).multiLoad( 1, 699, 2 ); - assertEquals( 2, list.size() ); + { + final List list = session.findMultiple( SimpleEntity.class, List.of(1, 699, 2), OrderedReturn.UNORDERED ); + assertEquals( 2, list.size() ); + } } ); } @@ -257,14 +276,26 @@ public void testBasicMultiLoadWithManagedAndNoChecking(SessionFactoryScope scope public void testBasicMultiLoadWithManagedAndChecking(SessionFactoryScope scope) { scope.inTransaction( session -> { - SimpleEntity first = session.byId( SimpleEntity.class ).load( 1 ); - List list = session.byMultipleIds( SimpleEntity.class ) - .enableSessionCheck( true ) - .multiLoad( ids( 56 ) ); - assertEquals( 56, list.size() ); - // this check is HIGHLY specific to implementation in the batch loader - // which puts existing managed entities first... - assertSame( first, list.get( 0 ) ); + SimpleEntity first; + { + final List list; + first = session.byId( SimpleEntity.class ).load( 1 ); + list = session.byMultipleIds( SimpleEntity.class ) + .enableSessionCheck( true ) + .multiLoad( ids( 56 ) ); + assertEquals( 56, list.size() ); + // this check is HIGHLY specific to implementation in the batch loader + // which puts existing managed entities first... + assertSame( first, list.get( 0 ) ); + } + + { + final List list = session.findMultiple( SimpleEntity.class, idList(56), SessionChecking.ENABLED ); + assertEquals( 56, list.size() ); + // this check is HIGHLY specific to implementation in the batch loader + // which puts existing managed entities first... + assertSame( first, list.get( 0 ) ); + } } ); } @@ -288,13 +319,26 @@ public void testBasicMultiLoadWithManagedAndCheckingProxied(SessionFactoryScope scope.inTransaction( session -> { SimpleEntity first = session.byId( SimpleEntity.class ).getReference( 1 ); - List list = session.byMultipleIds( SimpleEntity.class ) - .enableSessionCheck( true ) - .multiLoad( ids( 56 ) ); - assertEquals( 56, list.size() ); - // this check is HIGHLY specific to implementation in the batch loader - // which puts existing managed entities first... - assertSame( first, list.get( 0 ) ); + { + final List list = session.byMultipleIds( SimpleEntity.class ) + .enableSessionCheck( true ) + .multiLoad( ids( 56 ) ); + assertEquals( 56, list.size() ); + // this check is HIGHLY specific to implementation in the batch loader + // which puts existing managed entities first... + assertSame( first, list.get( 0 ) ); + } + + session.evict( first ); + first = session.byId( SimpleEntity.class ).getReference( 1 ); + + { + final List list = session.findMultiple( SimpleEntity.class, idList(56), SessionChecking.ENABLED ); + assertEquals( 56, list.size() ); + // this check is HIGHLY specific to implementation in the batch loader + // which puts existing managed entities first... + assertSame( first, list.get( 0 ) ); + } } ); } @@ -330,31 +374,59 @@ public void testMultiLoadFrom2ndLevelCache(SessionFactoryScope scope) { // Validate that the entity is still in the Level 2 cache assertTrue( session.getSessionFactory().getCache().containsEntity( SimpleEntity.class, 2 ) ); - statementInspector.getSqlQueries().clear(); - - // Multiload 3 items and ensure that multiload pulls 2 from the database & 1 from the cache. - List entities = session.byMultipleIds( SimpleEntity.class ) - .with( CacheMode.NORMAL ) - .enableSessionCheck( true ) - .multiLoad( ids( 3 ) ); - assertEquals( 3, entities.size() ); - assertEquals( 1, statistics.getSecondLevelCacheHitCount() ); - - for(SimpleEntity entity: entities) { - assertTrue( session.contains( entity ) ); + statementInspector.clear(); + { + // Multiload 3 items and ensure that multiload pulls 2 from the database & 1 from the cache. + final List entities = session.byMultipleIds( SimpleEntity.class ) + .with( CacheMode.NORMAL ) + .enableSessionCheck( true ) + .multiLoad( ids( 3 ) ); + assertEquals( 3, entities.size() ); + assertEquals( 1, statistics.getSecondLevelCacheHitCount() ); + + for ( SimpleEntity entity : entities ) { + assertTrue( session.contains( entity ) ); + } + + final int paramCount = StringHelper.countUnquoted( + statementInspector.getSqlQueries().get( 0 ), + '?' + ); + + final Dialect dialect = session.getSessionFactory().getJdbcServices().getDialect(); + if ( MultiKeyLoadHelper.supportsSqlArrayType( dialect ) ) { + assertThat( paramCount, is( 1 ) ); + } + else { + assertThat( paramCount, is( 2 ) ); + } } - final int paramCount = StringHelper.countUnquoted( - statementInspector.getSqlQueries().get( 0 ), - '?' - ); - - final Dialect dialect = session.getSessionFactory().getJdbcServices().getDialect(); - if ( MultiKeyLoadHelper.supportsSqlArrayType( dialect ) ) { - assertThat( paramCount, is( 1 ) ); - } - else { - assertThat( paramCount, is( 2 ) ); + { + // Multiload 3 items and ensure that multiload pulls 2 from the database & 1 from the cache. + final List entities = session.findMultiple( SimpleEntity.class, idList( 3 ), + CacheMode.NORMAL, + SessionChecking.ENABLED + ); + assertEquals( 3, entities.size() ); + assertEquals( 1, statistics.getSecondLevelCacheHitCount() ); + + for ( SimpleEntity entity : entities ) { + assertTrue( session.contains( entity ) ); + } + + final int paramCount = StringHelper.countUnquoted( + statementInspector.getSqlQueries().get( 0 ), + '?' + ); + + final Dialect dialect = session.getSessionFactory().getJdbcServices().getDialect(); + if ( MultiKeyLoadHelper.supportsSqlArrayType( dialect ) ) { + assertThat( paramCount, is( 1 ) ); + } + else { + assertThat( paramCount, is( 2 ) ); + } } } ); @@ -392,31 +464,60 @@ public void testUnorderedMultiLoadFrom2ndLevelCache(SessionFactoryScope scope) { // Validate that the entity is still in the Level 2 cache assertTrue( session.getSessionFactory().getCache().containsEntity( SimpleEntity.class, 2 ) ); - statementInspector.getSqlQueries().clear(); - - // Multiload 3 items and ensure that multiload pulls 2 from the database & 1 from the cache. - List entities = session.byMultipleIds( SimpleEntity.class ) - .with( CacheMode.NORMAL ) - .enableSessionCheck( true ) - .enableOrderedReturn( false ) - .multiLoad( ids( 3 ) ); - assertEquals( 3, entities.size() ); - assertEquals( 1, statistics.getSecondLevelCacheHitCount() ); + statementInspector.clear(); - for ( SimpleEntity entity : entities ) { - assertTrue( session.contains( entity ) ); + { + // Multiload 3 items and ensure that multiload pulls 2 from the database & 1 from the cache. + final List entities = session.byMultipleIds( SimpleEntity.class ) + .with( CacheMode.NORMAL ) + .enableSessionCheck( true ) + .enableOrderedReturn( false ) + .multiLoad( ids( 3 ) ); + assertEquals( 3, entities.size() ); + assertEquals( 1, statistics.getSecondLevelCacheHitCount() ); + + for ( SimpleEntity entity : entities ) { + assertTrue( session.contains( entity ) ); + } + final int paramCount = StringHelper.countUnquoted( + statementInspector.getSqlQueries().get( 0 ), + '?' + ); + + final Dialect dialect = session.getSessionFactory().getJdbcServices().getDialect(); + if ( MultiKeyLoadHelper.supportsSqlArrayType( dialect ) ) { + assertThat( paramCount, is( 1 ) ); + } + else { + assertThat( paramCount, is( 2 ) ); + } } - final int paramCount = StringHelper.countUnquoted( - statementInspector.getSqlQueries().get( 0 ), - '?' - ); - final Dialect dialect = session.getSessionFactory().getJdbcServices().getDialect(); - if ( MultiKeyLoadHelper.supportsSqlArrayType( dialect ) ) { - assertThat( paramCount, is( 1 ) ); - } - else { - assertThat( paramCount, is( 2 ) ); + { + // Multiload 3 items and ensure that multiload pulls 2 from the database & 1 from the cache. + final List entities = session.findMultiple( SimpleEntity.class, idList( 3 ), + CacheMode.NORMAL, + SessionChecking.ENABLED, + OrderedReturn.UNORDERED + ); + assertEquals( 3, entities.size() ); + assertEquals( 1, statistics.getSecondLevelCacheHitCount() ); + + for ( SimpleEntity entity : entities ) { + assertTrue( session.contains( entity ) ); + } + final int paramCount = StringHelper.countUnquoted( + statementInspector.getSqlQueries().get( 0 ), + '?' + ); + + final Dialect dialect = session.getSessionFactory().getJdbcServices().getDialect(); + if ( MultiKeyLoadHelper.supportsSqlArrayType( dialect ) ) { + assertThat( paramCount, is( 1 ) ); + } + else { + assertThat( paramCount, is( 2 ) ); + } } } ); @@ -434,27 +535,54 @@ public void testOrderedMultiLoadFrom2ndLevelCachePendingDelete(SessionFactorySco statementInspector.clear(); - // Multi-load 3 items and ensure that it pulls 2 from the database & 1 from the cache. - List entities = session.byMultipleIds( SimpleEntity.class ) - .with( CacheMode.NORMAL ) - .enableSessionCheck( true ) - .enableOrderedReturn( true ) - .multiLoad( ids( 3 ) ); - assertEquals( 3, entities.size() ); - - assertNull( entities.get( 1 ) ); - - final int paramCount = StringHelper.countUnquoted( - statementInspector.getSqlQueries().get( 0 ), - '?' - ); - - final Dialect dialect = session.getSessionFactory().getJdbcServices().getDialect(); - if ( MultiKeyLoadHelper.supportsSqlArrayType( dialect ) ) { - assertThat( paramCount, is( 1 ) ); + { + // Multi-load 3 items and ensure that it pulls 2 from the database & 1 from the cache. + final List entities = session.byMultipleIds( SimpleEntity.class ) + .with( CacheMode.NORMAL ) + .enableSessionCheck( true ) + .enableOrderedReturn( true ) + .multiLoad( ids( 3 ) ); + assertEquals( 3, entities.size() ); + + assertNull( entities.get( 1 ) ); + + final int paramCount = StringHelper.countUnquoted( + statementInspector.getSqlQueries().get( 0 ), + '?' + ); + + final Dialect dialect = session.getSessionFactory().getJdbcServices().getDialect(); + if ( MultiKeyLoadHelper.supportsSqlArrayType( dialect ) ) { + assertThat( paramCount, is( 1 ) ); + } + else { + assertThat( paramCount, is( 2 ) ); + } } - else { - assertThat( paramCount, is( 2 ) ); + + { + // Multi-load 3 items and ensure that it pulls 2 from the database & 1 from the cache. + final List entities = session.findMultiple( SimpleEntity.class, idList( 3 ), + CacheMode.NORMAL, + SessionChecking.ENABLED, + OrderedReturn.ORDERED + ); + assertEquals( 3, entities.size() ); + + assertNull( entities.get( 1 ) ); + + final int paramCount = StringHelper.countUnquoted( + statementInspector.getSqlQueries().get( 0 ), + '?' + ); + + final Dialect dialect = session.getSessionFactory().getJdbcServices().getDialect(); + if ( MultiKeyLoadHelper.supportsSqlArrayType( dialect ) ) { + assertThat( paramCount, is( 1 ) ); + } + else { + assertThat( paramCount, is( 2 ) ); + } } } ); @@ -472,33 +600,66 @@ public void testOrderedMultiLoadFrom2ndLevelCachePendingDeleteReturnRemoved(Sess statementInspector.clear(); - // Multiload 3 items and ensure that multiload pulls 2 from the database & 1 from the cache. - List entities = session.byMultipleIds( SimpleEntity.class ) - .with( CacheMode.NORMAL ) - .enableSessionCheck( true ) - .enableOrderedReturn( true ) - .enableReturnOfDeletedEntities( true ) - .multiLoad( ids( 3 ) ); - assertEquals( 3, entities.size() ); - - SimpleEntity deletedEntity = entities.get( 1 ); - assertNotNull( deletedEntity ); - - final EntityEntry entry = session.getPersistenceContext() - .getEntry( deletedEntity ); - assertTrue( entry.getStatus().isDeletedOrGone() ); - - final int paramCount = StringHelper.countUnquoted( - statementInspector.getSqlQueries().get( 0 ), - '?' - ); - - final Dialect dialect = session.getSessionFactory().getJdbcServices().getDialect(); - if ( MultiKeyLoadHelper.supportsSqlArrayType( dialect ) ) { - assertThat( paramCount, is( 1 ) ); + { + // Multiload 3 items and ensure that multiload pulls 2 from the database & 1 from the cache. + final List entities = session.byMultipleIds( SimpleEntity.class ) + .with( CacheMode.NORMAL ) + .enableSessionCheck( true ) + .enableOrderedReturn( true ) + .enableReturnOfDeletedEntities( true ) + .multiLoad( ids( 3 ) ); + assertEquals( 3, entities.size() ); + + SimpleEntity deletedEntity = entities.get( 1 ); + assertNotNull( deletedEntity ); + + EntityEntry entry = session.getPersistenceContext() + .getEntry( deletedEntity ); + assertTrue( entry.getStatus().isDeletedOrGone() ); + + final int paramCount = StringHelper.countUnquoted( + statementInspector.getSqlQueries().get( 0 ), + '?' + ); + + final Dialect dialect = session.getSessionFactory().getJdbcServices().getDialect(); + if ( MultiKeyLoadHelper.supportsSqlArrayType( dialect ) ) { + assertThat( paramCount, is( 1 ) ); + } + else { + assertThat( paramCount, is( 2 ) ); + } } - else { - assertThat( paramCount, is( 2 ) ); + + { + // Multiload 3 items and ensure that multiload pulls 2 from the database & 1 from the cache. + final List entities = session.findMultiple( SimpleEntity.class, idList( 3 ), + CacheMode.NORMAL, + SessionChecking.ENABLED, + OrderedReturn.ORDERED, + IncludeRemovals.INCLUDE + ); + assertEquals( 3, entities.size() ); + + SimpleEntity deletedEntity = entities.get( 1 ); + assertNotNull( deletedEntity ); + + EntityEntry entry = session.getPersistenceContext() + .getEntry( deletedEntity ); + assertTrue( entry.getStatus().isDeletedOrGone() ); + + final int paramCount = StringHelper.countUnquoted( + statementInspector.getSqlQueries().get( 0 ), + '?' + ); + + final Dialect dialect = session.getSessionFactory().getJdbcServices().getDialect(); + if ( MultiKeyLoadHelper.supportsSqlArrayType( dialect ) ) { + assertThat( paramCount, is( 1 ) ); + } + else { + assertThat( paramCount, is( 2 ) ); + } } } ); } @@ -515,27 +676,54 @@ public void testUnorderedMultiLoadFrom2ndLevelCachePendingDelete(SessionFactoryS statementInspector.clear(); - // Multiload 3 items and ensure that multiload pulls 2 from the database & 1 from the cache. - List entities = session.byMultipleIds( SimpleEntity.class ) - .with( CacheMode.NORMAL ) - .enableSessionCheck( true ) - .enableOrderedReturn( false ) - .multiLoad( ids( 3 ) ); - assertEquals( 3, entities.size() ); - - assertTrue( entities.stream().anyMatch( Objects::isNull ) ); - - final int paramCount = StringHelper.countUnquoted( - statementInspector.getSqlQueries().get( 0 ), - '?' - ); - - final Dialect dialect = session.getSessionFactory().getJdbcServices().getDialect(); - if ( MultiKeyLoadHelper.supportsSqlArrayType( dialect ) ) { - assertThat( paramCount, is( 1 ) ); + { + // Multiload 3 items and ensure that multiload pulls 2 from the database & 1 from the cache. + final List entities = session.byMultipleIds( SimpleEntity.class ) + .with( CacheMode.NORMAL ) + .enableSessionCheck( true ) + .enableOrderedReturn( false ) + .multiLoad( ids( 3 ) ); + assertEquals( 3, entities.size() ); + + assertTrue( entities.stream().anyMatch( Objects::isNull ) ); + + final int paramCount = StringHelper.countUnquoted( + statementInspector.getSqlQueries().get( 0 ), + '?' + ); + + final Dialect dialect = session.getSessionFactory().getJdbcServices().getDialect(); + if ( MultiKeyLoadHelper.supportsSqlArrayType( dialect ) ) { + assertThat( paramCount, is( 1 ) ); + } + else { + assertThat( paramCount, is( 2 ) ); + } } - else { - assertThat( paramCount, is( 2 ) ); + + { + // Multiload 3 items and ensure that multiload pulls 2 from the database & 1 from the cache. + final List entities = session.findMultiple( SimpleEntity.class, idList( 3 ), + CacheMode.NORMAL, + SessionChecking.ENABLED, + OrderedReturn.UNORDERED + ); + assertEquals( 3, entities.size() ); + + assertTrue( entities.stream().anyMatch( Objects::isNull ) ); + + final int paramCount = StringHelper.countUnquoted( + statementInspector.getSqlQueries().get( 0 ), + '?' + ); + + final Dialect dialect = session.getSessionFactory().getJdbcServices().getDialect(); + if ( MultiKeyLoadHelper.supportsSqlArrayType( dialect ) ) { + assertThat( paramCount, is( 1 ) ); + } + else { + assertThat( paramCount, is( 2 ) ); + } } } ); } @@ -552,33 +740,66 @@ public void testUnorderedMultiLoadFrom2ndLevelCachePendingDeleteReturnRemoved(Se statementInspector.clear(); - // Multiload 3 items and ensure that multiload pulls 2 from the database & 1 from the cache. - List entities = session.byMultipleIds( SimpleEntity.class ) - .with( CacheMode.NORMAL ) - .enableSessionCheck( true ) - .enableOrderedReturn( false ) - .enableReturnOfDeletedEntities( true ) - .multiLoad( ids( 3 ) ); - assertEquals( 3, entities.size() ); - - SimpleEntity deletedEntity = entities.stream().filter( simpleEntity -> simpleEntity.getId() - .equals( 2 ) ).findAny().orElse( null ); - assertNotNull( deletedEntity ); - - final EntityEntry entry = session.getPersistenceContext().getEntry( deletedEntity ); - assertTrue( entry.getStatus().isDeletedOrGone() ); + { + // Multiload 3 items and ensure that multiload pulls 2 from the database & 1 from the cache. + final List entities = session.byMultipleIds( SimpleEntity.class ) + .with( CacheMode.NORMAL ) + .enableSessionCheck( true ) + .enableOrderedReturn( false ) + .enableReturnOfDeletedEntities( true ) + .multiLoad( ids( 3 ) ); + assertEquals( 3, entities.size() ); + + SimpleEntity deletedEntity = entities.stream().filter( simpleEntity -> simpleEntity.getId() + .equals( 2 ) ).findAny().orElse( null ); + assertNotNull( deletedEntity ); + + EntityEntry entry = session.getPersistenceContext().getEntry( deletedEntity ); + assertTrue( entry.getStatus().isDeletedOrGone() ); + + final int paramCount = StringHelper.countUnquoted( + statementInspector.getSqlQueries().get( 0 ), + '?' + ); + + final Dialect dialect = session.getSessionFactory().getJdbcServices().getDialect(); + if ( MultiKeyLoadHelper.supportsSqlArrayType( dialect ) ) { + assertThat( paramCount, is( 1 ) ); + } + else { + assertThat( paramCount, is( 2 ) ); + } + } - final int paramCount = StringHelper.countUnquoted( - statementInspector.getSqlQueries().get( 0 ), - '?' + { + // Multiload 3 items and ensure that multiload pulls 2 from the database & 1 from the cache. + final List entities =session.findMultiple( SimpleEntity.class, idList( 3 ), + CacheMode.NORMAL, + SessionChecking.ENABLED, + OrderedReturn.UNORDERED, + IncludeRemovals.INCLUDE ); - - final Dialect dialect = session.getSessionFactory().getJdbcServices().getDialect(); - if ( MultiKeyLoadHelper.supportsSqlArrayType( dialect ) ) { - assertThat( paramCount, is( 1 ) ); - } - else { - assertThat( paramCount, is( 2 ) ); + assertEquals( 3, entities.size() ); + + SimpleEntity deletedEntity = entities.stream().filter( simpleEntity -> simpleEntity.getId() + .equals( 2 ) ).findAny().orElse( null ); + assertNotNull( deletedEntity ); + + EntityEntry entry = session.getPersistenceContext().getEntry( deletedEntity ); + assertTrue( entry.getStatus().isDeletedOrGone() ); + + final int paramCount = StringHelper.countUnquoted( + statementInspector.getSqlQueries().get( 0 ), + '?' + ); + + final Dialect dialect = session.getSessionFactory().getJdbcServices().getDialect(); + if ( MultiKeyLoadHelper.supportsSqlArrayType( dialect ) ) { + assertThat( paramCount, is( 1 ) ); + } + else { + assertThat( paramCount, is( 2 ) ); + } } } ); } @@ -615,15 +836,24 @@ public void testMultiLoadClearsBatchFetchQueue(SessionFactoryScope scope) { .containsEntityKey( entityKey ) ); // now bulk load, which should clean up the BatchFetchQueue entry - List list = session.byMultipleIds( SimpleEntity.class ) - .enableSessionCheck( true ) - .multiLoad( ids( 56 ) ); - - assertEquals( 56, list.size() ); - assertFalse( session.getPersistenceContext() - .getBatchFetchQueue() - .containsEntityKey( entityKey ) ); + { + final List list = session.byMultipleIds( SimpleEntity.class ) + .enableSessionCheck( true ) + .multiLoad( ids( 56 ) ); + assertEquals( 56, list.size() ); + assertFalse( session.getPersistenceContext() + .getBatchFetchQueue() + .containsEntityKey( entityKey ) ); + } + { + final List list = session.findMultiple( SimpleEntity.class, idList( 56 ), + SessionChecking.ENABLED ); + assertEquals( 56, list.size() ); + assertFalse( session.getPersistenceContext() + .getBatchFetchQueue() + .containsEntityKey( entityKey ) ); + } } ); } @@ -636,6 +866,14 @@ private Integer[] ids(int count) { return ids; } + private List idList(int count) { + List ids = new ArrayList<>(count); + for ( int i = 1; i <= count; i++ ) { + ids.add(i); + } + return ids; + } + @Entity( name = "SimpleEntity" ) @Table( name = "SimpleEntity" ) @Cacheable() From 5c0b942014027c2f9a206deb1203a150df93038a Mon Sep 17 00:00:00 2001 From: Steve Ebersole Date: Mon, 6 Oct 2025 14:53:46 -0600 Subject: [PATCH 2/3] HHH-19829 - Deprecate MultiIdentifierLoadAccess and the Session.byMultipleIds() methods --- .../chapters/pc/PersistenceContext.adoc | 99 +++---------------- .../java/org/hibernate/IncludeRemovals.java | 29 ++++-- .../java/org/hibernate/MultiFindOption.java | 8 ++ .../java/org/hibernate/OrderedReturn.java | 41 ++++++-- .../src/main/java/org/hibernate/Session.java | 23 ++--- .../java/org/hibernate/SessionChecking.java | 30 ++++-- .../orm/test/pc/FindMultipleDocTests.java | 48 +++++++++ whats-new.adoc | 12 +++ 8 files changed, 163 insertions(+), 127 deletions(-) create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/pc/FindMultipleDocTests.java diff --git a/documentation/src/main/asciidoc/userguide/chapters/pc/PersistenceContext.adoc b/documentation/src/main/asciidoc/userguide/chapters/pc/PersistenceContext.adoc index 62eef58a76d7..89f70efc8348 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/pc/PersistenceContext.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/pc/PersistenceContext.adoc @@ -187,102 +187,31 @@ If you want to load multiple entities by providing their identifiers, calling th but also inefficient. While the Jakarta Persistence standard does not support retrieving multiple entities at once, other than running a JPQL or Criteria API query, -Hibernate offers this functionality via the -https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/Session.html#byMultipleIds-java.lang.Class-[`byMultipleIds` method] of the Hibernate `Session`. - -The `byMultipleIds` method returns a -https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/MultiIdentifierLoadAccess.html[`MultiIdentifierLoadAccess`] -which you can use to customize the multi-load request. - -The `MultiIdentifierLoadAccess` interface provides several methods which you can use to -change the behavior of the multi-load call: - -`enableOrderedReturn(boolean enabled)`:: -This setting controls whether the returned `List` is ordered and positional in relation to the -incoming ids. If enabled (the default), the return `List` is ordered and -positional relative to the incoming ids. In other words, a request to -`multiLoad([2,1,3])` will return `[Entity#2, Entity#1, Entity#3]`. -+ -An important distinction is made here in regards to the handling of -unknown entities depending on this "ordered return" setting. -If enabled, a null is inserted into the `List` at the proper position(s). -If disabled, the nulls are not put into the return List. -+ -In other words, consumers of the returned ordered List would need to be able to handle null elements. -`enableSessionCheck(boolean enabled)`:: -This setting, which is disabled by default, tells Hibernate to check the first-level cache (a.k.a `Session` or Persistence Context) first and, if the entity is found and already managed by the Hibernate `Session`, the cached entity will be added to the returned `List`, therefore skipping it from being fetched via the multi-load query. -`enableReturnOfDeletedEntities(boolean enabled)`:: -This setting instructs Hibernate if the multi-load operation is allowed to return entities that were deleted by the current Persistence Context. A deleted entity is one which has been passed to this -`Session.delete` or `Session.remove` method, but the `Session` was not flushed yet, meaning that the -associated row was not deleted in the database table. -+ -The default behavior is to handle them as null in the return (see `enableOrderedReturn`). -When enabled, the result set will contain deleted entities. -When disabled (which is the default behavior), deleted entities are not included in the returning `List`. -`with(LockOptions lockOptions)`:: -This setting allows you to pass a given -https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/LockOptions.html[`LockOptions`] mode to the multi-load query. -`with(CacheMode cacheMode)`:: -This setting allows you to pass a given -https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/CacheMode.html[`CacheMode`] -strategy so that we can load entities from the second-level cache, therefore skipping the cached entities from being fetched via the multi-load query. -`withBatchSize(int batchSize)`:: -This setting allows you to specify a batch size for loading the entities (e.g. how many at a time). -+ -The default is to use a batch sizing strategy defined by the `Dialect.getDefaultBatchLoadSizingStrategy()` method. -+ -Any greater-than-one value here will override that default behavior. -`with(RootGraph graph)`:: -The `RootGraph` is a Hibernate extension to the Jakarta Persistence `EntityGraph` contract, -and this method allows you to pass a specific `RootGraph` to the multi-load query -so that it can fetch additional relationships of the current loading entity. +Hibernate offers this functionality via the `Session#findMultiple` methods which accepts a list of identifiers to load and a group of options which control certain behaviors of the loading - + +* `ReadOnlyMode` - whether the entities loaded should be marked as read-only. +* `LockMode` (`LockModeType`) - a lock mode to be applied +* `Timeout` - if a pessimistic lock mode is used, a timeout to allow +* `Locking.Scope` (PessimisticLockScope`) - if a pessimistic lock mode is used, what scope should it be applied +* `Locking.FollowOn` - allow (or not) Hibernate to acquire locks through additional queries if needed +* `CacheMode` (`CacheStoreMode` / `CacheRetrieveMode`) - how second level caching should be used, if at all +* `BatchSize` - how many identifiers should be loaded from the database at once +* `SessionChecking` - whether to look into the persistence context to check entity state +* `IncludeRemovals` - if `SessionChecking` is enabled, how removed entities should be handled +* `OrderedReturn` - whether the results should be ordered according to the order of the passed identifiers Now, assuming we have 3 `Person` entities in the database, we can load all of them with a single call as illustrated by the following example: [[tag::pc-by-multiple-ids-example]] -.Loading multiple entities using the `byMultipleIds()` Hibernate API +.Loading multiple entities using the `findMultiple()` Hibernate API ==== [source, java, indent=0] ---- -include::{example-dir-pc}/MultiLoadIdTest.java[tags=pc-by-multiple-ids-example] +include::{example-dir-pc}/FindMultipleDocTests.java[tags=pc-find-multiple-example] ---- - -[source, SQL, indent=0] ----- -include::{extrasdir}/pc-by-multiple-ids-example.sql[] ----- -==== - -Notice that only one SQL SELECT statement was executed since the second call uses the -https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/MultiIdentifierLoadAccess.html#enableSessionCheck-boolean-[`enableSessionCheck`] method of the `MultiIdentifierLoadAccess` -to instruct Hibernate to skip entities that are already loaded in the current Persistence Context. - -If the entities are not available in the current Persistence Context but they could be loaded from the second-level cache, you can use the -https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/MultiIdentifierLoadAccess.html#with-org.hibernate.CacheMode-[`with(CacheMode)`] method of the `MultiIdentifierLoadAccess` object. - -[[tag::pc-by-multiple-ids-second-level-cache-example]] -.Loading multiple entities from the second-level cache ==== -[source, java, indent=0] ----- -include::{example-dir-pc}/MultiLoadIdTest.java[tags=pc-by-multiple-ids-second-level-cache-example] ----- -==== - -In the example above, we first make sure that we clear the second-level cache to demonstrate that -the multi-load query will put the returning entities into the second-level cache. - -After executing the first `byMultipleIds` call, Hibernate is going to fetch the requested entities, -and as illustrated by the `getSecondLevelCachePutCount` method call, 3 entities were indeed added to the -shared cache. - -Afterward, when executing the second `byMultipleIds` call for the same entities in a new Hibernate `Session`, -we set the -https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/CacheMode.html#NORMAL[`CacheMode.NORMAL`] second-level cache mode so that entities are going to be returned from the second-level cache. - -The `getSecondLevelCacheHitCount` statistics method returns 3 this time, since the 3 entities were loaded from the second-level cache, and, as illustrated by `sqlStatementInterceptor.getSqlQueries()`, no multi-load SELECT statement was executed this time. [[pc-find-natural-id]] === Obtain an entity by natural-id diff --git a/hibernate-core/src/main/java/org/hibernate/IncludeRemovals.java b/hibernate-core/src/main/java/org/hibernate/IncludeRemovals.java index 243225a1a3e5..62de77873bff 100644 --- a/hibernate-core/src/main/java/org/hibernate/IncludeRemovals.java +++ b/hibernate-core/src/main/java/org/hibernate/IncludeRemovals.java @@ -11,21 +11,32 @@ import java.util.List; /** - * MultiFindOption implementation to specify whether the returned list - * of entity instances should contain instances that have been - * {@linkplain Session#remove(Object) marked for removal} in the - * current session, but not yet deleted in the database. + * When {@linkplain SessionChecking} is enabled, this option controls how + * to handle entities which are already contained by the persistence context + * but which are in a removed state (marked for removal, but not yet flushed). *

- * The default is {@link #EXCLUDE}, meaning that instances marked for - * removal are replaced by null in the returned list of entities when {@link OrderedReturn} - * is used. + * The default is {@link #EXCLUDE}. * - * @see org.hibernate.MultiFindOption - * @see OrderedReturn * @see org.hibernate.Session#findMultiple(Class, List, FindOption...) * @see org.hibernate.Session#findMultiple(EntityGraph, List , FindOption...) + * + * @since 7.2 */ public enum IncludeRemovals implements MultiFindOption { + /** + * Removed entities are included in the load result. + */ INCLUDE, + /** + * The default. Removed entities are excluded from the load result. + *

+ * When combined with {@linkplain OrderedReturn#UNORDERED}, the entity is + * simply excluded from the result. + *

+ * When combined with {@linkplain OrderedReturn#ORDERED}, the entity is replaced + * by {@code null} in the result. + * + * @see OrderedReturn + */ EXCLUDE } diff --git a/hibernate-core/src/main/java/org/hibernate/MultiFindOption.java b/hibernate-core/src/main/java/org/hibernate/MultiFindOption.java index 400f7b9354ad..264158f0a5e2 100644 --- a/hibernate-core/src/main/java/org/hibernate/MultiFindOption.java +++ b/hibernate-core/src/main/java/org/hibernate/MultiFindOption.java @@ -5,10 +5,18 @@ package org.hibernate; +import jakarta.persistence.EntityGraph; import jakarta.persistence.FindOption; +import java.util.List; + /** * Simple marker interface for FindOptions which can be applied to multiple id loading. + * + * @see org.hibernate.Session#findMultiple(Class, List, FindOption...) + * @see org.hibernate.Session#findMultiple(EntityGraph, List , FindOption...) + * + * @since 7.2 */ public interface MultiFindOption extends FindOption { } diff --git a/hibernate-core/src/main/java/org/hibernate/OrderedReturn.java b/hibernate-core/src/main/java/org/hibernate/OrderedReturn.java index 5ba775c590ab..78f76183967d 100644 --- a/hibernate-core/src/main/java/org/hibernate/OrderedReturn.java +++ b/hibernate-core/src/main/java/org/hibernate/OrderedReturn.java @@ -11,22 +11,43 @@ import java.util.List; /** - * MultiFindOption implementation to specify whether the returned list - * of entity instances should be ordered, where the position of an entity - * instance is determined by the position of its identifier - * in the list of ids passed to {@code findMultiple(...)}. + * Indicates whether the result list should be ordered relative to the + * position of the identifier list. E.g. + *

+ * List<Person> results = session.findMultiple(
+ *     Person.class,
+ *     List.of(1,2,3,2),
+ *     ORDERED
+ * );
+ * assert results.get(0).getId() == 1;
+ * assert results.get(1).getId() == 2;
+ * assert results.get(2).getId() == 3;
+ * assert results.get(3).getId() == 2;
+ * 
*

- * The default is {@link #ORDERED}, meaning the positions of the entities - * in the returned list correspond to the positions of their ids. In this case, - * the {@link IncludeRemovals} handling of entities marked for removal - * becomes important. + * The default is {@link #ORDERED}. * - * @see org.hibernate.MultiFindOption - * @see IncludeRemovals * @see org.hibernate.Session#findMultiple(Class, List, FindOption...) * @see org.hibernate.Session#findMultiple(EntityGraph, List , FindOption...) + * + * @since 7.2 */ public enum OrderedReturn implements MultiFindOption { + /** + * The default. The result list is ordered relative to the + * position of the identifiers list. This may result in {@code null} + * elements in the list -

+ *

+ * The result list will also always have the same length as the identifier list. + * + * @see IncludeRemovals + */ ORDERED, + /** + * The result list may be in any order. + */ UNORDERED } diff --git a/hibernate-core/src/main/java/org/hibernate/Session.java b/hibernate-core/src/main/java/org/hibernate/Session.java index 8d925c331240..4ab86a5ebc91 100644 --- a/hibernate-core/src/main/java/org/hibernate/Session.java +++ b/hibernate-core/src/main/java/org/hibernate/Session.java @@ -577,9 +577,6 @@ public interface Session extends SharedSessionContract, EntityManager { *

  • on the other hand, for databases with no SQL array type, a large batch size results * in long SQL statements with many JDBC parameters. * - *

    - * For more advanced cases, use {@link #byMultipleIds(Class)}, which returns an instance of - * {@link MultiIdentifierLoadAccess}. * * @param entityType the entity type * @param ids the list of identifiers @@ -588,7 +585,9 @@ public interface Session extends SharedSessionContract, EntityManager { * @return an ordered list of persistent instances, with null elements representing missing * entities, whose positions in the list match the positions of their ids in the * given list of identifiers - * @see #byMultipleIds(Class) + * + * @see MultiFindOption + * * @since 7.0 */ List findMultiple(Class entityType, List ids, FindOption... options); @@ -617,9 +616,6 @@ public interface Session extends SharedSessionContract, EntityManager { *

  • on the other hand, for databases with no SQL array type, a large batch size results * in long SQL statements with many JDBC parameters. * - *

    - * For more advanced cases, use {@link #byMultipleIds(Class)}, which returns an instance of - * {@link MultiIdentifierLoadAccess}. * * @param entityGraph the entity graph interpreted as a load graph * @param ids the list of identifiers @@ -628,7 +624,9 @@ public interface Session extends SharedSessionContract, EntityManager { * @return an ordered list of persistent instances, with null elements representing missing * entities, whose positions in the list match the positions of their ids in the * given list of identifiers - * @see #byMultipleIds(Class) + * + * @see MultiFindOption + * * @since 7.0 */ List findMultiple(EntityGraph entityGraph, List ids, FindOption... options); @@ -1166,9 +1164,7 @@ public interface Session extends SharedSessionContract, EntityManager { * * @see #findMultiple(Class, List, FindOption...) * - * @deprecated This method will be removed. - * Use {@link #findMultiple(Class, List, FindOption...)} instead. - * See {@link MultiFindOption}. + * @deprecated Use {@link #findMultiple(Class, List, FindOption...)} instead. */ @Deprecated(since = "7.2", forRemoval = true) MultiIdentifierLoadAccess byMultipleIds(Class entityClass); @@ -1183,9 +1179,8 @@ public interface Session extends SharedSessionContract, EntityManager { * * @throws HibernateException If the given name does not resolve to a mapped entity * - * @deprecated This method will be removed. - * Use {@link #findMultiple(Class, List, FindOption...)} instead. - * See {@link MultiFindOption}. + * @deprecated Use {@link #findMultiple(EntityGraph, List, FindOption...)} instead, + * with {@linkplain SessionFactory#createGraphForDynamicEntity(String)}. */ @Deprecated(since = "7.2", forRemoval = true) MultiIdentifierLoadAccess byMultipleIds(String entityName); diff --git a/hibernate-core/src/main/java/org/hibernate/SessionChecking.java b/hibernate-core/src/main/java/org/hibernate/SessionChecking.java index 37757af3655d..96f1d1e22cc0 100644 --- a/hibernate-core/src/main/java/org/hibernate/SessionChecking.java +++ b/hibernate-core/src/main/java/org/hibernate/SessionChecking.java @@ -11,20 +11,32 @@ import java.util.List; /** - * MultiFindOption implementation to specify whether the ids of managed entity instances already - * cached in the current persistence context should be excluded. - * from the list of ids sent to the database. - *

    - * The default is {@link #DISABLED}, meaning all ids are included and sent to the database. + * Indicates whether the persistence context should be checked for entities + * matching the identifiers to be loaded -

      + *
    • Entities which are in a managed state are not re-loaded from the database. + *
    • Entities which are in a removed state are {@linkplain IncludeRemovals#EXCLUDE excluded} + * from the result by default, but can be {@linkplain IncludeRemovals#INCLUDE included} if desired. + *
    + *

    + * The default is {@link #DISABLED} * - * Use {@link #ENABLED} to exclude already managed entity instance ids from - * the list of ids sent to the database. - * - * @see org.hibernate.MultiFindOption * @see org.hibernate.Session#findMultiple(Class, List , FindOption...) * @see org.hibernate.Session#findMultiple(EntityGraph, List , FindOption...) + * + * @since 7.2 */ public enum SessionChecking implements MultiFindOption { + /** + * The persistence context will be checked. Identifiers for entities already contained + * in the persistence context will not be sent to the database for loading. If the + * entity is marked for removal in the persistence context, whether it is returned + * is controlled by {@linkplain IncludeRemovals}. + * + * @see IncludeRemovals + */ ENABLED, + /** + * The default. All identifiers to be loaded will be read from the database and returned. + */ DISABLED } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/pc/FindMultipleDocTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/pc/FindMultipleDocTests.java new file mode 100644 index 000000000000..03205913f0f2 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/pc/FindMultipleDocTests.java @@ -0,0 +1,48 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.pc; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.hibernate.LockMode.PESSIMISTIC_WRITE; +import static org.hibernate.OrderedReturn.ORDERED; + +/** + * @author Steve Ebersole + */ +@SuppressWarnings("JUnitMalformedDeclaration") +@DomainModel(annotatedClasses = FindMultipleDocTests.Person.class) +@SessionFactory +public class FindMultipleDocTests { + @Test + void testUsage(SessionFactoryScope factoryScope) { + factoryScope.inTransaction( (session) -> { + //tag::pc-find-multiple-example[] + List persons = session.findMultiple( + Person.class, + List.of(1,2,3), + PESSIMISTIC_WRITE, + ORDERED + ); + //end::pc-find-multiple-example[] + } ); + } + + @Entity(name="Person") + @Table(name="persons") + public static class Person { + @Id + private Integer id; + private String name; + } +} diff --git a/whats-new.adoc b/whats-new.adoc index 44eddb0010db..c8fb24fe072a 100644 --- a/whats-new.adoc +++ b/whats-new.adoc @@ -59,6 +59,18 @@ class Person { The annotation is only legal on top-level embedded. Placement on nested embedded values will be ignored. +[[MultiFindOption]] +== Introduction of MultiFindOption + +Previous versions of Hibernate supported loading multiple entities of a type via the `Session#byMultipleIds` method. +7.0 added `Session#findMultiple` methods which accepted `FindOption` configuration; but, for options specific to multiple-id loading, users still had to revert to `Session#byMultipleIds`. +7.2 covers this gap and introduces new `MultiFindOption` configuration - + +* `SessionChecking` +* `OrderedReturn` +* `IncludeRemovals` + +`Session#byMultipleIds` and `MultiIdentifierLoadAccess` have been deprecated. [[child-stateless-sessions]] == Child StatelessSession From 54c2d38a07e23763cbfcc7bde6a43b9ca4889f9a Mon Sep 17 00:00:00 2001 From: Steve Ebersole Date: Tue, 7 Oct 2025 10:03:33 -0600 Subject: [PATCH 3/3] HHH-19829 - Deprecate MultiIdentifierLoadAccess and the Session.byMultipleIds() methods --- .../chapters/pc/PersistenceContext.adoc | 8 +- ...indOption.java => FindMultipleOption.java} | 2 +- .../{OrderedReturn.java => OrderingMode.java} | 12 +- ...IncludeRemovals.java => RemovalsMode.java} | 18 +-- .../src/main/java/org/hibernate/Session.java | 4 +- ...ionChecking.java => SessionCheckMode.java} | 11 +- .../MultiIdentifierLoadAccessImpl.java | 27 +++-- .../NaturalIdMultiLoadAccessStandard.java | 18 +-- .../org/hibernate/internal/SessionImpl.java | 16 +-- .../internal/StatelessSessionImpl.java | 15 ++- .../internal/AbstractMultiIdEntityLoader.java | 47 +++++--- .../AbstractMultiNaturalIdLoader.java | 11 +- .../loader/ast/spi/MultiIdLoadOptions.java | 15 ++- .../loader/ast/spi/MultiLoadOptions.java | 31 ++++- .../test/dynamicmap/FindOperationTests.java | 12 +- .../test/loading/multiLoad/MultiLoadTest.java | 42 +++---- .../orm/test/pc/FindMultipleDocTests.java | 110 +++++++++++++++++- whats-new.adoc | 8 +- 18 files changed, 283 insertions(+), 124 deletions(-) rename hibernate-core/src/main/java/org/hibernate/{MultiFindOption.java => FindMultipleOption.java} (89%) rename hibernate-core/src/main/java/org/hibernate/{OrderedReturn.java => OrderingMode.java} (71%) rename hibernate-core/src/main/java/org/hibernate/{IncludeRemovals.java => RemovalsMode.java} (55%) rename hibernate-core/src/main/java/org/hibernate/{SessionChecking.java => SessionCheckMode.java} (76%) diff --git a/documentation/src/main/asciidoc/userguide/chapters/pc/PersistenceContext.adoc b/documentation/src/main/asciidoc/userguide/chapters/pc/PersistenceContext.adoc index 89f70efc8348..c815a9960461 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/pc/PersistenceContext.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/pc/PersistenceContext.adoc @@ -192,13 +192,13 @@ Hibernate offers this functionality via the `Session#findMultiple` methods which * `ReadOnlyMode` - whether the entities loaded should be marked as read-only. * `LockMode` (`LockModeType`) - a lock mode to be applied * `Timeout` - if a pessimistic lock mode is used, a timeout to allow -* `Locking.Scope` (PessimisticLockScope`) - if a pessimistic lock mode is used, what scope should it be applied +* `Locking.Scope` (PessimisticLockScope`) - if a pessimistic lock mode is used, what scope should it be applied to * `Locking.FollowOn` - allow (or not) Hibernate to acquire locks through additional queries if needed * `CacheMode` (`CacheStoreMode` / `CacheRetrieveMode`) - how second level caching should be used, if at all * `BatchSize` - how many identifiers should be loaded from the database at once -* `SessionChecking` - whether to look into the persistence context to check entity state -* `IncludeRemovals` - if `SessionChecking` is enabled, how removed entities should be handled -* `OrderedReturn` - whether the results should be ordered according to the order of the passed identifiers +* `SessionCheckMode` - whether to look into the persistence context to check entity state +* `RemovalsMode` - if `SessionCheckMode` is enabled, how removed entities should be handled +* `OrderingMode` - whether the results should be ordered according to the order of the passed identifiers Now, assuming we have 3 `Person` entities in the database, we can load all of them with a single call as illustrated by the following example: diff --git a/hibernate-core/src/main/java/org/hibernate/MultiFindOption.java b/hibernate-core/src/main/java/org/hibernate/FindMultipleOption.java similarity index 89% rename from hibernate-core/src/main/java/org/hibernate/MultiFindOption.java rename to hibernate-core/src/main/java/org/hibernate/FindMultipleOption.java index 264158f0a5e2..fa7fe213c8fa 100644 --- a/hibernate-core/src/main/java/org/hibernate/MultiFindOption.java +++ b/hibernate-core/src/main/java/org/hibernate/FindMultipleOption.java @@ -18,5 +18,5 @@ * * @since 7.2 */ -public interface MultiFindOption extends FindOption { +public interface FindMultipleOption extends FindOption { } diff --git a/hibernate-core/src/main/java/org/hibernate/OrderedReturn.java b/hibernate-core/src/main/java/org/hibernate/OrderingMode.java similarity index 71% rename from hibernate-core/src/main/java/org/hibernate/OrderedReturn.java rename to hibernate-core/src/main/java/org/hibernate/OrderingMode.java index 78f76183967d..a464c79f4f90 100644 --- a/hibernate-core/src/main/java/org/hibernate/OrderedReturn.java +++ b/hibernate-core/src/main/java/org/hibernate/OrderingMode.java @@ -32,18 +32,12 @@ * * @since 7.2 */ -public enum OrderedReturn implements MultiFindOption { +public enum OrderingMode implements FindMultipleOption { /** * The default. The result list is ordered relative to the - * position of the identifiers list. This may result in {@code null} - * elements in the list -

      - *
    • non-existent identifiers - *
    • removed entities (when combined with {@linkplain IncludeRemovals#EXCLUDE}) - *
    - *

    - * The result list will also always have the same length as the identifier list. + * position of the identifiers list. * - * @see IncludeRemovals + * @see RemovalsMode */ ORDERED, /** diff --git a/hibernate-core/src/main/java/org/hibernate/IncludeRemovals.java b/hibernate-core/src/main/java/org/hibernate/RemovalsMode.java similarity index 55% rename from hibernate-core/src/main/java/org/hibernate/IncludeRemovals.java rename to hibernate-core/src/main/java/org/hibernate/RemovalsMode.java index 62de77873bff..7986d94c0e7e 100644 --- a/hibernate-core/src/main/java/org/hibernate/IncludeRemovals.java +++ b/hibernate-core/src/main/java/org/hibernate/RemovalsMode.java @@ -11,32 +11,24 @@ import java.util.List; /** - * When {@linkplain SessionChecking} is enabled, this option controls how + * When {@linkplain SessionCheckMode} is enabled, this option controls how * to handle entities which are already contained by the persistence context * but which are in a removed state (marked for removal, but not yet flushed). *

    - * The default is {@link #EXCLUDE}. + * The default is {@link #REPLACE}. * * @see org.hibernate.Session#findMultiple(Class, List, FindOption...) * @see org.hibernate.Session#findMultiple(EntityGraph, List , FindOption...) * * @since 7.2 */ -public enum IncludeRemovals implements MultiFindOption { +public enum RemovalsMode implements FindMultipleOption { /** * Removed entities are included in the load result. */ INCLUDE, /** - * The default. Removed entities are excluded from the load result. - *

    - * When combined with {@linkplain OrderedReturn#UNORDERED}, the entity is - * simply excluded from the result. - *

    - * When combined with {@linkplain OrderedReturn#ORDERED}, the entity is replaced - * by {@code null} in the result. - * - * @see OrderedReturn + * The default. Removed entities are replaced with {@code null} in the load result. */ - EXCLUDE + REPLACE } diff --git a/hibernate-core/src/main/java/org/hibernate/Session.java b/hibernate-core/src/main/java/org/hibernate/Session.java index 4ab86a5ebc91..9d9ae1593468 100644 --- a/hibernate-core/src/main/java/org/hibernate/Session.java +++ b/hibernate-core/src/main/java/org/hibernate/Session.java @@ -586,7 +586,7 @@ public interface Session extends SharedSessionContract, EntityManager { * entities, whose positions in the list match the positions of their ids in the * given list of identifiers * - * @see MultiFindOption + * @see FindMultipleOption * * @since 7.0 */ @@ -625,7 +625,7 @@ public interface Session extends SharedSessionContract, EntityManager { * entities, whose positions in the list match the positions of their ids in the * given list of identifiers * - * @see MultiFindOption + * @see FindMultipleOption * * @since 7.0 */ diff --git a/hibernate-core/src/main/java/org/hibernate/SessionChecking.java b/hibernate-core/src/main/java/org/hibernate/SessionCheckMode.java similarity index 76% rename from hibernate-core/src/main/java/org/hibernate/SessionChecking.java rename to hibernate-core/src/main/java/org/hibernate/SessionCheckMode.java index 96f1d1e22cc0..705bdc7503f7 100644 --- a/hibernate-core/src/main/java/org/hibernate/SessionChecking.java +++ b/hibernate-core/src/main/java/org/hibernate/SessionCheckMode.java @@ -14,8 +14,9 @@ * Indicates whether the persistence context should be checked for entities * matching the identifiers to be loaded -

      *
    • Entities which are in a managed state are not re-loaded from the database. - *
    • Entities which are in a removed state are {@linkplain IncludeRemovals#EXCLUDE excluded} - * from the result by default, but can be {@linkplain IncludeRemovals#INCLUDE included} if desired. + * those identifiers are removed from the SQL restriction sent to the database. + *
    • Entities which are in a removed state are {@linkplain RemovalsMode#REPLACE excluded} + * from the result by default, but can be {@linkplain RemovalsMode#INCLUDE included} if desired. *
    *

    * The default is {@link #DISABLED} @@ -25,14 +26,14 @@ * * @since 7.2 */ -public enum SessionChecking implements MultiFindOption { +public enum SessionCheckMode implements FindMultipleOption { /** * The persistence context will be checked. Identifiers for entities already contained * in the persistence context will not be sent to the database for loading. If the * entity is marked for removal in the persistence context, whether it is returned - * is controlled by {@linkplain IncludeRemovals}. + * is controlled by {@linkplain RemovalsMode}. * - * @see IncludeRemovals + * @see RemovalsMode */ ENABLED, /** diff --git a/hibernate-core/src/main/java/org/hibernate/internal/MultiIdentifierLoadAccessImpl.java b/hibernate-core/src/main/java/org/hibernate/internal/MultiIdentifierLoadAccessImpl.java index ab3ab06a513d..c2c3d43304be 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/MultiIdentifierLoadAccessImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/MultiIdentifierLoadAccessImpl.java @@ -11,6 +11,9 @@ import org.hibernate.LockMode; import org.hibernate.LockOptions; import org.hibernate.MultiIdentifierLoadAccess; +import org.hibernate.OrderingMode; +import org.hibernate.RemovalsMode; +import org.hibernate.SessionCheckMode; import org.hibernate.UnknownProfileException; import org.hibernate.engine.spi.EffectiveEntityGraph; import org.hibernate.engine.spi.LoadQueryInfluencers; @@ -43,9 +46,9 @@ class MultiIdentifierLoadAccessImpl implements MultiIdentifierLoadAccess, private GraphSemantic graphSemantic; private Integer batchSize; - private boolean sessionCheckingEnabled; - private boolean returnOfDeletedEntitiesEnabled; - private boolean orderedReturnEnabled = true; + private SessionCheckMode sessionCheckMode = SessionCheckMode.DISABLED; + private RemovalsMode removalsMode = RemovalsMode.REPLACE; + protected OrderingMode orderingMode = OrderingMode.ORDERED; private Set enabledFetchProfiles; private Set disabledFetchProfiles; @@ -116,8 +119,8 @@ public MultiIdentifierLoadAccess withBatchSize(int batchSize) { } @Override - public boolean isSessionCheckingEnabled() { - return sessionCheckingEnabled; + public SessionCheckMode getSessionCheckMode() { + return sessionCheckMode; } @Override @@ -127,29 +130,29 @@ public boolean isSecondLevelCacheCheckingEnabled() { @Override public MultiIdentifierLoadAccess enableSessionCheck(boolean enabled) { - this.sessionCheckingEnabled = enabled; + this.sessionCheckMode = enabled ? SessionCheckMode.ENABLED : SessionCheckMode.DISABLED; return this; } @Override - public boolean isReturnOfDeletedEntitiesEnabled() { - return returnOfDeletedEntitiesEnabled; + public RemovalsMode getRemovalsMode() { + return removalsMode; } @Override public MultiIdentifierLoadAccess enableReturnOfDeletedEntities(boolean enabled) { - this.returnOfDeletedEntitiesEnabled = enabled; + this.removalsMode = enabled ? RemovalsMode.INCLUDE : RemovalsMode.REPLACE; return this; } @Override - public boolean isOrderReturnEnabled() { - return orderedReturnEnabled; + public OrderingMode getOrderingMode() { + return orderingMode; } @Override public MultiIdentifierLoadAccess enableOrderedReturn(boolean enabled) { - this.orderedReturnEnabled = enabled; + this.orderingMode = enabled ? OrderingMode.ORDERED : OrderingMode.UNORDERED; return this; } diff --git a/hibernate-core/src/main/java/org/hibernate/internal/NaturalIdMultiLoadAccessStandard.java b/hibernate-core/src/main/java/org/hibernate/internal/NaturalIdMultiLoadAccessStandard.java index 61f010701235..f6be988fca2c 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/NaturalIdMultiLoadAccessStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/NaturalIdMultiLoadAccessStandard.java @@ -14,6 +14,8 @@ import org.hibernate.LockMode; import org.hibernate.LockOptions; import org.hibernate.NaturalIdMultiLoadAccess; +import org.hibernate.OrderingMode; +import org.hibernate.RemovalsMode; import org.hibernate.engine.spi.EffectiveEntityGraph; import org.hibernate.engine.spi.LoadQueryInfluencers; import org.hibernate.engine.spi.SharedSessionContractImplementor; @@ -38,8 +40,8 @@ public class NaturalIdMultiLoadAccessStandard implements NaturalIdMultiLoadAc private GraphSemantic graphSemantic; private Integer batchSize; - private boolean returnOfDeletedEntitiesEnabled; - private boolean orderedReturnEnabled = true; + private RemovalsMode removalsMode = RemovalsMode.REPLACE; + private OrderingMode orderingMode = OrderingMode.ORDERED; public NaturalIdMultiLoadAccessStandard(EntityPersister entityDescriptor, SharedSessionContractImplementor session) { this.entityDescriptor = entityDescriptor; @@ -92,13 +94,13 @@ public NaturalIdMultiLoadAccess withBatchSize(int batchSize) { @Override public NaturalIdMultiLoadAccess enableReturnOfDeletedEntities(boolean enabled) { - returnOfDeletedEntitiesEnabled = enabled; + this.removalsMode = enabled ? RemovalsMode.INCLUDE : RemovalsMode.REPLACE; return this; } @Override public NaturalIdMultiLoadAccess enableOrderedReturn(boolean enabled) { - orderedReturnEnabled = enabled; + this.orderingMode = enabled ? OrderingMode.ORDERED : OrderingMode.UNORDERED; return this; } @@ -163,13 +165,13 @@ public List multiLoad(List ids) { } @Override - public boolean isReturnOfDeletedEntitiesEnabled() { - return returnOfDeletedEntitiesEnabled; + public RemovalsMode getRemovalsMode() { + return removalsMode; } @Override - public boolean isOrderReturnEnabled() { - return orderedReturnEnabled; + public OrderingMode getOrderingMode() { + return orderingMode; } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java b/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java index 550560ee94f1..8b6d9b98b142 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java @@ -964,14 +964,14 @@ else if ( option instanceof ReadOnlyMode ) { else if ( option instanceof BatchSize batchSizeOption ) { batchSize = batchSizeOption.batchSize(); } - else if ( option instanceof SessionChecking sessionChecking ) { - loadAccess.enableSessionCheck( option == sessionChecking.ENABLED ); + else if ( option instanceof SessionCheckMode sessionCheckMode ) { + loadAccess.enableSessionCheck( option == sessionCheckMode.ENABLED ); } - else if ( option instanceof OrderedReturn orderedReturn ) { - loadAccess.enableOrderedReturn( option == orderedReturn.ORDERED ); + else if ( option instanceof OrderingMode orderingMode ) { + loadAccess.enableOrderedReturn( option == orderingMode.ORDERED ); } - else if ( option instanceof IncludeRemovals includeRemovals ) { - loadAccess.enableReturnOfDeletedEntities( option == includeRemovals.INCLUDE ); + else if ( option instanceof RemovalsMode removalsMode ) { + loadAccess.enableReturnOfDeletedEntities( option == removalsMode.INCLUDE ); } } loadAccess.with( lockOptions ) @@ -2300,8 +2300,8 @@ else if ( option instanceof EnabledFetchProfile enabledFetchProfile ) { else if ( option instanceof ReadOnlyMode ) { loadAccess.withReadOnly( option == ReadOnlyMode.READ_ONLY ); } - else if ( option instanceof MultiFindOption multiFindOption ) { - throw new IllegalArgumentException( "Option '" + multiFindOption + "' can only be used in 'findMultiple()'" ); + else if ( option instanceof FindMultipleOption findMultipleOption ) { + throw new IllegalArgumentException( "Option '" + findMultipleOption + "' can only be used in 'findMultiple()'" ); } } if ( lockOptions.getLockMode().isPessimistic() ) { diff --git a/hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java b/hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java index eee651170480..596b9e108900 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java @@ -12,6 +12,9 @@ import org.hibernate.HibernateException; import org.hibernate.LockMode; import org.hibernate.LockOptions; +import org.hibernate.OrderingMode; +import org.hibernate.RemovalsMode; +import org.hibernate.SessionCheckMode; import org.hibernate.SessionException; import org.hibernate.StatelessSession; import org.hibernate.TransientObjectException; @@ -1494,8 +1497,8 @@ private MultiLoadOptions(LockMode lockMode) { } @Override - public boolean isSessionCheckingEnabled() { - return false; + public SessionCheckMode getSessionCheckMode() { + return SessionCheckMode.DISABLED; } @Override @@ -1509,13 +1512,13 @@ public Boolean getReadOnly(SessionImplementor session) { } @Override - public boolean isReturnOfDeletedEntitiesEnabled() { - return false; + public RemovalsMode getRemovalsMode() { + return RemovalsMode.REPLACE; } @Override - public boolean isOrderReturnEnabled() { - return true; + public OrderingMode getOrderingMode() { + return OrderingMode.ORDERED; } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/AbstractMultiIdEntityLoader.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/AbstractMultiIdEntityLoader.java index 08ef24ce5b65..24e8f5f49fbb 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/AbstractMultiIdEntityLoader.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/AbstractMultiIdEntityLoader.java @@ -7,6 +7,9 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.hibernate.LockMode; import org.hibernate.LockOptions; +import org.hibernate.OrderingMode; +import org.hibernate.RemovalsMode; +import org.hibernate.SessionCheckMode; import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.engine.spi.EntityKey; import org.hibernate.engine.spi.SessionFactoryImplementor; @@ -75,7 +78,7 @@ public EntityMappingType getLoadable() { @Override public final List load(K[] ids, MultiIdLoadOptions loadOptions, SharedSessionContractImplementor session) { assert ids != null; - return loadOptions.isOrderReturnEnabled() + return loadOptions.getOrderingMode() == OrderingMode.ORDERED ? performOrderedMultiLoad( ids, loadOptions, session ) : performUnorderedMultiLoad( ids, loadOptions, session ); } @@ -84,7 +87,7 @@ private List performUnorderedMultiLoad( Object[] ids, MultiIdLoadOptions loadOptions, SharedSessionContractImplementor session) { - assert !loadOptions.isOrderReturnEnabled(); + assert loadOptions.getOrderingMode() == OrderingMode.UNORDERED; assert ids != null; if ( MULTI_KEY_LOAD_LOGGER.isTraceEnabled() ) { MULTI_KEY_LOAD_LOGGER.unorderedBatchLoadStarting( getLoadable().getEntityName() ); @@ -96,7 +99,7 @@ private List performOrderedMultiLoad( Object[] ids, MultiIdLoadOptions loadOptions, SharedSessionContractImplementor session) { - assert loadOptions.isOrderReturnEnabled(); + assert loadOptions.getOrderingMode() == OrderingMode.ORDERED; assert ids != null; if ( MULTI_KEY_LOAD_LOGGER.isTraceEnabled() ) { MULTI_KEY_LOAD_LOGGER.orderedMultiLoadStarting( getLoadable().getEntityName() ); @@ -174,7 +177,7 @@ private void handleResults( final Object result; if ( entity == null // the entity is locally deleted, and the options ask that we not return such entities - || !loadOptions.isReturnOfDeletedEntitiesEnabled() + || loadOptions.getRemovalsMode() == RemovalsMode.REPLACE && persistenceContext.getEntry( entity ).getStatus().isDeletedOrGone() ) { result = null; } @@ -200,7 +203,8 @@ private boolean loadFromEnabledCaches( EntityKey entityKey, List result, int i) { - return ( loadOptions.isSessionCheckingEnabled() || loadOptions.isSecondLevelCacheCheckingEnabled() ) + return (loadOptions.getSessionCheckMode() == SessionCheckMode.ENABLED + || loadOptions.isSecondLevelCacheCheckingEnabled() ) && isLoadFromCaches( loadOptions, entityKey, lockOptions, result, i, session ); } @@ -211,16 +215,20 @@ private boolean isLoadFromCaches( List results, int i, SharedSessionContractImplementor session) { - if ( loadOptions.isSessionCheckingEnabled() ) { + if ( loadOptions.getSessionCheckMode() == SessionCheckMode.ENABLED ) { // look for it in the Session first final var entry = loadFromSessionCache( entityKey, lockOptions, GET, session ); final Object entity = entry.entity(); if ( entity != null ) { // put a null in the results - final Object result = - loadOptions.isReturnOfDeletedEntitiesEnabled() - || entry.isManaged() - ? entity : null; + final Object result; + if ( loadOptions.getRemovalsMode() == RemovalsMode.INCLUDE + || entry.isManaged() ) { + result = entity; + } + else { + result = null; + } results.add( i, result ); return true; } @@ -245,9 +253,16 @@ protected List unorderedMultiLoad( SharedSessionContractImplementor session) { final var lockOptions = lockOptions( loadOptions ); final List results = arrayList( ids.length ); - final var unresolvableIds = - resolveInCachesIfEnabled( ids, loadOptions, lockOptions, session, - (position, entityKey, resolvedRef) -> results.add( (T) resolvedRef ) ); + final var unresolvableIds = resolveInCachesIfEnabled( + ids, + loadOptions, + lockOptions, + session, + (position, entityKey, resolvedRef) -> { + //noinspection unchecked + results.add( (T) resolvedRef ); + } + ); if ( !isEmpty( unresolvableIds ) ) { loadEntitiesWithUnresolvedIds( unresolvableIds, loadOptions, lockOptions, results, session ); final var batchFetchQueue = session.getPersistenceContextInternal().getBatchFetchQueue(); @@ -277,7 +292,7 @@ private Object[] resolveInCachesIfEnabled( @NonNull LockOptions lockOptions, SharedSessionContractImplementor session, ResolutionConsumer resolutionConsumer) { - return loadOptions.isSessionCheckingEnabled() || loadOptions.isSecondLevelCacheCheckingEnabled() + return loadOptions.getSessionCheckMode() == SessionCheckMode.ENABLED || loadOptions.isSecondLevelCacheCheckingEnabled() // the user requested that we exclude ids corresponding to already managed // entities from the generated load SQL. So here we will iterate all // incoming id values and see whether it corresponds to an existing @@ -358,10 +373,10 @@ private List loadFromCaches( // look for it in the Session first final var entry = loadFromSessionCache( entityKey, lockOptions, GET, session ); final Object sessionEntity; - if ( loadOptions.isSessionCheckingEnabled() ) { + if ( loadOptions.getSessionCheckMode() == SessionCheckMode.ENABLED ) { sessionEntity = entry.entity(); if ( sessionEntity != null - && !loadOptions.isReturnOfDeletedEntitiesEnabled() + && loadOptions.getRemovalsMode() == RemovalsMode.REPLACE && !entry.isManaged() ) { resolutionConsumer.consume( i, entityKey, null ); return unresolvedIds; diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/AbstractMultiNaturalIdLoader.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/AbstractMultiNaturalIdLoader.java index 722752976f8a..8ac139d2d38d 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/AbstractMultiNaturalIdLoader.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/AbstractMultiNaturalIdLoader.java @@ -6,6 +6,8 @@ import org.hibernate.LockOptions; +import org.hibernate.OrderingMode; +import org.hibernate.RemovalsMode; import org.hibernate.engine.spi.EntityKey; import org.hibernate.engine.spi.PersistenceContext; import org.hibernate.engine.spi.SharedSessionContractImplementor; @@ -39,7 +41,7 @@ public List multiLoad(K[] naturalIds, MultiNaturalIdLoadOptions options, return emptyList(); } else { - return options.isOrderReturnEnabled() + return options.getOrderingMode() == OrderingMode.ORDERED ? performOrderedMultiLoad( naturalIds, options, session ) : performUnorderedMultiLoad( naturalIds, options, session ); } @@ -114,13 +116,14 @@ private List sortResults( final Object result; if ( entity == null // the entity is locally deleted, and the options ask that we not return such entities - || !loadOptions.isReturnOfDeletedEntitiesEnabled() - && context.getEntry( entity ).getStatus().isDeletedOrGone() ) { + || loadOptions.getRemovalsMode() == RemovalsMode.REPLACE + && context.getEntry( entity ).getStatus().isDeletedOrGone() ) { result = null; } else { result = context.proxyFor( entity ); } + //noinspection unchecked results.add( (E) result ); } return results; @@ -147,7 +150,7 @@ private Object[] checkPersistenceContextForCachedResults( if ( entity != null ) { // Entity is already in the persistence context final var entry = context.getEntry( entity ); - if ( loadOptions.isReturnOfDeletedEntitiesEnabled() + if ( loadOptions.getRemovalsMode() == RemovalsMode.INCLUDE || !entry.getStatus().isDeletedOrGone() ) { // either a managed entry, or a deleted one with returnDeleted enabled upgradeLock( entity, entry, lockOptions, session ); diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiIdLoadOptions.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiIdLoadOptions.java index e8cae3f75a25..b1417d1ff3e5 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiIdLoadOptions.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiIdLoadOptions.java @@ -4,19 +4,32 @@ */ package org.hibernate.loader.ast.spi; +import org.hibernate.SessionCheckMode; import org.hibernate.engine.spi.SessionImplementor; /** * Encapsulation of the options for loading multiple entities by id */ public interface MultiIdLoadOptions extends MultiLoadOptions { + /** + * Controls whether to check the current status of each identified entity + * within the persistence context. + * + * @since 7.2 + */ + SessionCheckMode getSessionCheckMode(); + /** * Check the first-level cache first, and only if the entity is not found in the cache * should Hibernate hit the database. * * @return the session cache is checked first + * @deprecated Use {@linkplain #getSessionCheckMode()} instead. */ - boolean isSessionCheckingEnabled(); + @Deprecated(since = "7.2", forRemoval = true) + default boolean isSessionCheckingEnabled() { + return getSessionCheckMode() == SessionCheckMode.ENABLED; + } /** * Check the second-level cache first, and only if the entity is not found in the cache diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiLoadOptions.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiLoadOptions.java index 0e711cb38f30..2279ad8279c8 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiLoadOptions.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/spi/MultiLoadOptions.java @@ -5,24 +5,51 @@ package org.hibernate.loader.ast.spi; import org.hibernate.LockOptions; +import org.hibernate.OrderingMode; +import org.hibernate.RemovalsMode; /** * Base contract for options for multi-load operations */ public interface MultiLoadOptions { + /** + * How should entities in removed status be handled. + * + * @since 7.2 + */ + RemovalsMode getRemovalsMode(); + + /** + * Whether the result should be ordered relative to the order of + * identifiers to load. + * + * @since 7.2 + */ + OrderingMode getOrderingMode(); + /** * Should we returned entities that are scheduled for deletion. * * @return entities that are scheduled for deletion are returned as well. + * + * @deprecated Use {@linkplain #getRemovalsMode()} instead. */ - boolean isReturnOfDeletedEntitiesEnabled(); + @Deprecated(since = "7.2", forRemoval = true) + default boolean isReturnOfDeletedEntitiesEnabled() { + return getRemovalsMode() == RemovalsMode.INCLUDE; + } /** * Should the entities be returned in the same order as their associated entity identifiers were provided. * * @return entities follow the provided identifier order + * + * @deprecated Use {@linkplain #getOrderingMode()} instead. */ - boolean isOrderReturnEnabled(); + @Deprecated(since = "7.2", forRemoval = true) + default boolean isOrderReturnEnabled() { + return getOrderingMode() == OrderingMode.ORDERED; + } /** * Specify the lock options applied during loading. diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/dynamicmap/FindOperationTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/dynamicmap/FindOperationTests.java index d2b6069a8da8..e2cce114a003 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/dynamicmap/FindOperationTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/dynamicmap/FindOperationTests.java @@ -4,10 +4,10 @@ */ package org.hibernate.orm.test.dynamicmap; -import org.hibernate.IncludeRemovals; -import org.hibernate.OrderedReturn; +import org.hibernate.RemovalsMode; +import org.hibernate.OrderingMode; import org.hibernate.ReadOnlyMode; -import org.hibernate.SessionChecking; +import org.hibernate.SessionCheckMode; import org.hibernate.graph.RootGraph; import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.SessionFactory; @@ -65,9 +65,9 @@ void testFindWithOptions(SessionFactoryScope factoryScope) { @Test void testFindWithIllegalOptions(SessionFactoryScope factoryScope) { factoryScope.inTransaction( (session) -> { - assertThrows( IllegalArgumentException.class, () ->session.find( "artist", 1, SessionChecking.ENABLED ) ); - assertThrows( IllegalArgumentException.class, () ->session.find( "artist", 1, OrderedReturn.ORDERED ) ); - assertThrows( IllegalArgumentException.class, () ->session.find( "artist", 1, IncludeRemovals.INCLUDE ) ); + assertThrows( IllegalArgumentException.class, () ->session.find( "artist", 1, SessionCheckMode.ENABLED ) ); + assertThrows( IllegalArgumentException.class, () ->session.find( "artist", 1, OrderingMode.ORDERED ) ); + assertThrows( IllegalArgumentException.class, () ->session.find( "artist", 1, RemovalsMode.INCLUDE ) ); } ); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/loading/multiLoad/MultiLoadTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/loading/multiLoad/MultiLoadTest.java index fa143411e7b4..fa81e2f1c021 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/loading/multiLoad/MultiLoadTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/loading/multiLoad/MultiLoadTest.java @@ -10,9 +10,9 @@ import org.hibernate.CacheMode; import org.hibernate.Hibernate; -import org.hibernate.IncludeRemovals; -import org.hibernate.OrderedReturn; -import org.hibernate.SessionChecking; +import org.hibernate.RemovalsMode; +import org.hibernate.OrderingMode; +import org.hibernate.SessionCheckMode; import org.hibernate.annotations.BatchSize; import org.hibernate.cache.spi.access.AccessType; import org.hibernate.cfg.AvailableSettings; @@ -226,7 +226,7 @@ public void testDuplicatedRequestedIdswithDisableOrderedReturn(SessionFactorySco } { - final List list = session.findMultiple( SimpleEntity.class, List.of( 1, 2, 3, 2, 2 ), OrderedReturn.UNORDERED ); + final List list = session.findMultiple( SimpleEntity.class, List.of( 1, 2, 3, 2, 2 ), OrderingMode.UNORDERED ); assertEquals( 3, list.size() ); } @@ -251,7 +251,7 @@ public void testNonExistentIdRequest(SessionFactoryScope scope) { } { - final List list = session.findMultiple( SimpleEntity.class, List.of(1, 699, 2), OrderedReturn.UNORDERED ); + final List list = session.findMultiple( SimpleEntity.class, List.of(1, 699, 2), OrderingMode.UNORDERED ); assertEquals( 2, list.size() ); } } @@ -290,7 +290,7 @@ public void testBasicMultiLoadWithManagedAndChecking(SessionFactoryScope scope) } { - final List list = session.findMultiple( SimpleEntity.class, idList(56), SessionChecking.ENABLED ); + final List list = session.findMultiple( SimpleEntity.class, idList(56), SessionCheckMode.ENABLED ); assertEquals( 56, list.size() ); // this check is HIGHLY specific to implementation in the batch loader // which puts existing managed entities first... @@ -333,7 +333,7 @@ public void testBasicMultiLoadWithManagedAndCheckingProxied(SessionFactoryScope first = session.byId( SimpleEntity.class ).getReference( 1 ); { - final List list = session.findMultiple( SimpleEntity.class, idList(56), SessionChecking.ENABLED ); + final List list = session.findMultiple( SimpleEntity.class, idList(56), SessionCheckMode.ENABLED ); assertEquals( 56, list.size() ); // this check is HIGHLY specific to implementation in the batch loader // which puts existing managed entities first... @@ -406,7 +406,7 @@ public void testMultiLoadFrom2ndLevelCache(SessionFactoryScope scope) { // Multiload 3 items and ensure that multiload pulls 2 from the database & 1 from the cache. final List entities = session.findMultiple( SimpleEntity.class, idList( 3 ), CacheMode.NORMAL, - SessionChecking.ENABLED + SessionCheckMode.ENABLED ); assertEquals( 3, entities.size() ); assertEquals( 1, statistics.getSecondLevelCacheHitCount() ); @@ -497,8 +497,8 @@ public void testUnorderedMultiLoadFrom2ndLevelCache(SessionFactoryScope scope) { // Multiload 3 items and ensure that multiload pulls 2 from the database & 1 from the cache. final List entities = session.findMultiple( SimpleEntity.class, idList( 3 ), CacheMode.NORMAL, - SessionChecking.ENABLED, - OrderedReturn.UNORDERED + SessionCheckMode.ENABLED, + OrderingMode.UNORDERED ); assertEquals( 3, entities.size() ); assertEquals( 1, statistics.getSecondLevelCacheHitCount() ); @@ -564,8 +564,8 @@ public void testOrderedMultiLoadFrom2ndLevelCachePendingDelete(SessionFactorySco // Multi-load 3 items and ensure that it pulls 2 from the database & 1 from the cache. final List entities = session.findMultiple( SimpleEntity.class, idList( 3 ), CacheMode.NORMAL, - SessionChecking.ENABLED, - OrderedReturn.ORDERED + SessionCheckMode.ENABLED, + OrderingMode.ORDERED ); assertEquals( 3, entities.size() ); @@ -635,9 +635,9 @@ public void testOrderedMultiLoadFrom2ndLevelCachePendingDeleteReturnRemoved(Sess // Multiload 3 items and ensure that multiload pulls 2 from the database & 1 from the cache. final List entities = session.findMultiple( SimpleEntity.class, idList( 3 ), CacheMode.NORMAL, - SessionChecking.ENABLED, - OrderedReturn.ORDERED, - IncludeRemovals.INCLUDE + SessionCheckMode.ENABLED, + OrderingMode.ORDERED, + RemovalsMode.INCLUDE ); assertEquals( 3, entities.size() ); @@ -705,8 +705,8 @@ public void testUnorderedMultiLoadFrom2ndLevelCachePendingDelete(SessionFactoryS // Multiload 3 items and ensure that multiload pulls 2 from the database & 1 from the cache. final List entities = session.findMultiple( SimpleEntity.class, idList( 3 ), CacheMode.NORMAL, - SessionChecking.ENABLED, - OrderedReturn.UNORDERED + SessionCheckMode.ENABLED, + OrderingMode.UNORDERED ); assertEquals( 3, entities.size() ); @@ -775,9 +775,9 @@ public void testUnorderedMultiLoadFrom2ndLevelCachePendingDeleteReturnRemoved(Se // Multiload 3 items and ensure that multiload pulls 2 from the database & 1 from the cache. final List entities =session.findMultiple( SimpleEntity.class, idList( 3 ), CacheMode.NORMAL, - SessionChecking.ENABLED, - OrderedReturn.UNORDERED, - IncludeRemovals.INCLUDE + SessionCheckMode.ENABLED, + OrderingMode.UNORDERED, + RemovalsMode.INCLUDE ); assertEquals( 3, entities.size() ); @@ -848,7 +848,7 @@ public void testMultiLoadClearsBatchFetchQueue(SessionFactoryScope scope) { { final List list = session.findMultiple( SimpleEntity.class, idList( 56 ), - SessionChecking.ENABLED ); + SessionCheckMode.ENABLED ); assertEquals( 56, list.size() ); assertFalse( session.getPersistenceContext() .getBatchFetchQueue() diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/pc/FindMultipleDocTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/pc/FindMultipleDocTests.java index 03205913f0f2..4fe3b027c7f3 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/pc/FindMultipleDocTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/pc/FindMultipleDocTests.java @@ -7,15 +7,21 @@ import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.Table; +import org.hibernate.RemovalsMode; +import org.hibernate.OrderingMode; +import org.hibernate.SessionCheckMode; import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.List; +import static org.assertj.core.api.Assertions.assertThat; import static org.hibernate.LockMode.PESSIMISTIC_WRITE; -import static org.hibernate.OrderedReturn.ORDERED; +import static org.hibernate.OrderingMode.ORDERED; /** * @author Steve Ebersole @@ -24,8 +30,24 @@ @DomainModel(annotatedClasses = FindMultipleDocTests.Person.class) @SessionFactory public class FindMultipleDocTests { + @BeforeEach + void createTestData(SessionFactoryScope factoryScope) { + factoryScope.inTransaction( (session) -> { + session.persist( new Person( 1, "Paul" ) ); + session.persist( new Person( 2, "John" ) ); + session.persist( new Person( 3, "Ringo" ) ); + session.persist( new Person( 4, "George" ) ); + session.persist( new Person( 5, "David" ) ); + } ); + } + + @AfterEach + void dropTestData(SessionFactoryScope factoryScope) { + factoryScope.dropData(); + } + @Test - void testUsage(SessionFactoryScope factoryScope) { + void testBasicUsage(SessionFactoryScope factoryScope) { factoryScope.inTransaction( (session) -> { //tag::pc-find-multiple-example[] List persons = session.findMultiple( @@ -38,11 +60,95 @@ void testUsage(SessionFactoryScope factoryScope) { } ); } + @Test + void testReplaceRemovals(SessionFactoryScope factoryScope) { + factoryScope.inTransaction( (session) -> { + session.remove( session.find( Person.class, 5 ) ); + + List persons = session.findMultiple( + Person.class, + List.of(1,2,3,4,5), + SessionCheckMode.ENABLED, + RemovalsMode.REPLACE, + OrderingMode.UNORDERED + ); + assertThat( persons ).hasSize( 5 ); + assertThat( persons ).containsNull(); + } ); + } + + @Test + void testIncludeRemovals(SessionFactoryScope factoryScope) { + factoryScope.inTransaction( (session) -> { + session.remove( session.find( Person.class, 5 ) ); + + List persons = session.findMultiple( + Person.class, + List.of(1,2,3,4,5), + SessionCheckMode.ENABLED, + RemovalsMode.INCLUDE, + OrderingMode.UNORDERED + ); + assertThat( persons ).hasSize( 5 ); + assertThat( persons ).doesNotContainNull(); + } ); + } + + @Test + void testOrderedRemovals(SessionFactoryScope factoryScope) { + factoryScope.inTransaction( (session) -> { + session.remove( session.find( Person.class, 5 ) ); + + List persons = session.findMultiple( + Person.class, + List.of(1,2,3,4,5), + SessionCheckMode.ENABLED, + RemovalsMode.INCLUDE, + OrderingMode.ORDERED + ); + assertThat( persons ).hasSize( 5 ); + assertThat( persons ).doesNotContainNull(); + assertThat( persons ).map( Person::getId ).containsExactly( 1, 2, 3, 4, 5 ); + } ); + } + + @Test + void testOrderedReplacedRemovals(SessionFactoryScope factoryScope) { + factoryScope.inTransaction( (session) -> { + session.remove( session.find( Person.class, 5 ) ); + + List persons = session.findMultiple( + Person.class, + List.of(1,2,3,4,5), + SessionCheckMode.ENABLED, + RemovalsMode.REPLACE, + OrderingMode.ORDERED + ); + assertThat( persons ).hasSize( 5 ); + assertThat( persons ).containsNull(); + } ); + } + @Entity(name="Person") @Table(name="persons") public static class Person { @Id private Integer id; private String name; + + public Person() { + } + public Person(Integer id, String name) { + this.id = id; + this.name = name; + } + + public Integer getId() { + return id; + } + + public String getName() { + return name; + } } } diff --git a/whats-new.adoc b/whats-new.adoc index c8fb24fe072a..afcd01bdb278 100644 --- a/whats-new.adoc +++ b/whats-new.adoc @@ -64,11 +64,11 @@ The annotation is only legal on top-level embedded. Placement on nested embedde Previous versions of Hibernate supported loading multiple entities of a type via the `Session#byMultipleIds` method. 7.0 added `Session#findMultiple` methods which accepted `FindOption` configuration; but, for options specific to multiple-id loading, users still had to revert to `Session#byMultipleIds`. -7.2 covers this gap and introduces new `MultiFindOption` configuration - +7.2 covers this gap and introduces new `FindMultipleOption` configuration - -* `SessionChecking` -* `OrderedReturn` -* `IncludeRemovals` +* `SessionCheckMode` +* `OrderingMode` +* `RemovalsMode` `Session#byMultipleIds` and `MultiIdentifierLoadAccess` have been deprecated.