HHH-17948 make findAll() accept FindOptions

and add missing options to MultiIdentifierLoadAccess
This commit is contained in:
Gavin King 2024-09-09 12:00:30 +02:00
parent 62e1b0470e
commit 6e2ed7f1a0
15 changed files with 291 additions and 42 deletions

View File

@ -51,6 +51,15 @@ public interface MultiIdentifierLoadAccess<T> {
*/
MultiIdentifierLoadAccess<T> with(CacheMode cacheMode);
/**
* Specify whether the entities should be loaded in read-only mode.
*
* @see Session#setDefaultReadOnly(boolean)
*
* @since 7.0
*/
MultiIdentifierLoadAccess<T> withReadOnly(boolean readOnly);
/**
* Override the associations fetched by default by specifying
* the complete list of associations to be fetched as an
@ -88,6 +97,30 @@ public interface MultiIdentifierLoadAccess<T> {
*/
MultiIdentifierLoadAccess<T> with(RootGraph<T> graph, GraphSemantic semantic);
/**
* Customize the associations fetched by specifying a
* {@linkplain org.hibernate.annotations.FetchProfile fetch profile}
* that should be enabled during this operation.
* <p>
* This allows the {@linkplain Session#isFetchProfileEnabled(String)
* session-level fetch profiles} to be temporarily overridden.
*
* @since 7.0
*/
MultiIdentifierLoadAccess<T> enableFetchProfile(String profileName);
/**
* Customize the associations fetched by specifying a
* {@linkplain org.hibernate.annotations.FetchProfile fetch profile}
* that should be disabled during this operation.
* <p>
* This allows the {@linkplain Session#isFetchProfileEnabled(String)
* session-level fetch profiles} to be temporarily overridden.
*
* @since 7.0
*/
MultiIdentifierLoadAccess<T> disableFetchProfile(String profileName);
/**
* Specify a batch size, that is, how many entities should be
* fetched in each request to the database.

View File

@ -9,6 +9,7 @@ package org.hibernate;
import java.util.List;
import java.util.function.Consumer;
import jakarta.persistence.FindOption;
import org.hibernate.graph.RootGraph;
import org.hibernate.jdbc.Work;
import org.hibernate.query.Query;
@ -706,12 +707,13 @@ public interface Session extends SharedSessionContract, EntityManager {
*
* @param entityType the entity type
* @param ids the identifiers
* @param options options, if any
* @return an ordered list of persistent instances, with null elements representing missing
* entities
* @see #byMultipleIds(Class)
* @since 7.0
*/
<E> List<E> findAll(Class<E> entityType, List<Object> ids);
<E> List<E> findAll(Class<E> entityType, List<Object> ids, FindOption... options);
/**
* Return the persistent instance of the given entity class with the given identifier,
@ -922,7 +924,7 @@ public interface Session extends SharedSessionContract, EntityManager {
*
* @throws HibernateException If the given class does not resolve as a mapped entity
*
* @see #findAll(Class, List)
* @see #findAll(Class, List, FindOption...)
*/
<T> MultiIdentifierLoadAccess<T> byMultipleIds(Class<T> entityClass);

View File

@ -954,8 +954,8 @@ public class SessionDelegatorBaseImpl implements SessionImplementor {
}
@Override
public <E> List<E> findAll(Class<E> entityType, List<Object> ids) {
return delegate.findAll( entityType, ids );
public <E> List<E> findAll(Class<E> entityType, List<Object> ids, FindOption... options) {
return delegate.findAll( entityType, ids, options );
}
@Override

View File

@ -264,8 +264,8 @@ public class SessionLazyDelegator implements Session {
}
@Override
public <E> List<E> findAll(Class<E> entityType, List<Object> ids) {
return this.lazySession.get().findAll( entityType, ids );
public <E> List<E> findAll(Class<E> entityType, List<Object> ids, FindOption... options) {
return this.lazySession.get().findAll( entityType, ids, options );
}
@Override

View File

@ -6,13 +6,18 @@
*/
package org.hibernate.internal;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Supplier;
import org.hibernate.CacheMode;
import org.hibernate.LockOptions;
import org.hibernate.MultiIdentifierLoadAccess;
import org.hibernate.UnknownProfileException;
import org.hibernate.engine.spi.EffectiveEntityGraph;
import org.hibernate.engine.spi.LoadQueryInfluencers;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.graph.GraphSemantic;
import org.hibernate.graph.RootGraph;
import org.hibernate.graph.spi.RootGraphImplementor;
@ -20,6 +25,8 @@ import org.hibernate.loader.ast.internal.LoaderHelper;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.loader.ast.spi.MultiIdLoadOptions;
import static java.util.Collections.emptyList;
/**
* @author Steve Ebersole
*/
@ -29,6 +36,7 @@ class MultiIdentifierLoadAccessImpl<T> implements MultiIdentifierLoadAccess<T>,
private LockOptions lockOptions;
private CacheMode cacheMode;
private Boolean readOnly;
private RootGraphImplementor<T> rootGraph;
private GraphSemantic graphSemantic;
@ -38,6 +46,9 @@ class MultiIdentifierLoadAccessImpl<T> implements MultiIdentifierLoadAccess<T>,
private boolean returnOfDeletedEntitiesEnabled;
private boolean orderedReturnEnabled = true;
private Set<String> enabledFetchProfiles;
private Set<String> disabledFetchProfiles;
public MultiIdentifierLoadAccessImpl(SessionImpl session, EntityPersister entityPersister) {
this.session = session;
this.entityPersister = entityPersister;
@ -60,6 +71,12 @@ class MultiIdentifierLoadAccessImpl<T> implements MultiIdentifierLoadAccess<T>,
return this;
}
@Override
public MultiIdentifierLoadAccess<T> withReadOnly(boolean readOnly) {
this.readOnly = readOnly;
return this;
}
@Override
public MultiIdentifierLoadAccess<T> with(RootGraph<T> graph, GraphSemantic semantic) {
this.rootGraph = (RootGraphImplementor<T>) graph;
@ -121,6 +138,13 @@ class MultiIdentifierLoadAccessImpl<T> implements MultiIdentifierLoadAccess<T>,
return this;
}
@Override
public Boolean getReadOnly(SessionImplementor session) {
return readOnly != null
? readOnly
: session.getLoadQueryInfluencers().getReadOnly();
}
@Override
@SuppressWarnings( "unchecked" )
public <K> List<T> multiLoad(K... ids) {
@ -128,7 +152,7 @@ class MultiIdentifierLoadAccessImpl<T> implements MultiIdentifierLoadAccess<T>,
}
public List<T> perform(Supplier<List<T>> executor) {
CacheMode sessionCacheMode = session.getCacheMode();
final CacheMode sessionCacheMode = session.getCacheMode();
boolean cacheModeChanged = false;
if ( cacheMode != null ) {
// naive check for now...
@ -140,20 +164,17 @@ class MultiIdentifierLoadAccessImpl<T> implements MultiIdentifierLoadAccess<T>,
}
try {
if ( graphSemantic != null ) {
if ( rootGraph == null ) {
throw new IllegalArgumentException( "Graph semantic specified, but no RootGraph was supplied" );
}
session.getLoadQueryInfluencers().getEffectiveEntityGraph().applyGraph( rootGraph, graphSemantic );
}
final LoadQueryInfluencers influencers = session.getLoadQueryInfluencers();
final HashSet<String> fetchProfiles =
influencers.adjustFetchProfiles( disabledFetchProfiles, enabledFetchProfiles );
final EffectiveEntityGraph effectiveEntityGraph =
influencers.applyEntityGraph( rootGraph, graphSemantic );
try {
return executor.get();
}
finally {
if ( graphSemantic != null ) {
session.getLoadQueryInfluencers().getEffectiveEntityGraph().clear();
}
effectiveEntityGraph.clear();
influencers.setEnabledFetchProfileNames( fetchProfiles );
}
}
finally {
@ -168,8 +189,9 @@ class MultiIdentifierLoadAccessImpl<T> implements MultiIdentifierLoadAccess<T>,
@SuppressWarnings( "unchecked" )
public <K> List<T> multiLoad(List<K> ids) {
if ( ids.isEmpty() ) {
return Collections.emptyList();
return emptyList();
}
else {
return perform( () -> (List<T>) entityPersister.multiLoad(
ids.toArray( LoaderHelper.createTypedArray( ids.get( 0 ).getClass(), ids.size() ) ),
session,
@ -177,3 +199,31 @@ class MultiIdentifierLoadAccessImpl<T> implements MultiIdentifierLoadAccess<T>,
) );
}
}
@Override
public MultiIdentifierLoadAccess<T> enableFetchProfile(String profileName) {
if ( !session.getFactory().containsFetchProfileDefinition( profileName ) ) {
throw new UnknownProfileException( profileName );
}
if ( enabledFetchProfiles == null ) {
enabledFetchProfiles = new HashSet<>();
}
enabledFetchProfiles.add( profileName );
if ( disabledFetchProfiles != null ) {
disabledFetchProfiles.remove( profileName );
}
return this;
}
@Override
public MultiIdentifierLoadAccess<T> disableFetchProfile(String profileName) {
if ( disabledFetchProfiles == null ) {
disabledFetchProfiles = new HashSet<>();
}
disabledFetchProfiles.add( profileName );
if ( enabledFetchProfiles != null ) {
enabledFetchProfiles.remove( profileName );
}
return this;
}
}

View File

@ -944,22 +944,64 @@ public class SessionImpl
@Override @Deprecated
public Object load(String entityName, Object id) throws HibernateException {
return this.byId( entityName ).getReference( id );
return byId( entityName ).getReference( id );
}
private <T> MultiIdentifierLoadAccess<T> multiloadAccessWithOptions(Class<T> entityClass, FindOption[] options) {
final MultiIdentifierLoadAccess<T> loadAccess = byMultipleIds( entityClass );
CacheStoreMode storeMode = getCacheStoreMode();
CacheRetrieveMode retrieveMode = getCacheRetrieveMode();
LockOptions lockOptions = copySessionLockOptions();
for ( FindOption option : options ) {
if ( option instanceof CacheStoreMode cacheStoreMode ) {
storeMode = cacheStoreMode;
}
else if ( option instanceof CacheRetrieveMode cacheRetrieveMode ) {
retrieveMode = cacheRetrieveMode;
}
else if ( option instanceof CacheMode cacheMode ) {
storeMode = cacheMode.getJpaStoreMode();
retrieveMode = cacheMode.getJpaRetrieveMode();
}
else if ( option instanceof LockModeType lockModeType ) {
lockOptions.setLockMode( LockModeTypeHelper.getLockMode( lockModeType ) );
}
else if ( option instanceof LockMode lockMode ) {
lockOptions.setLockMode( lockMode );
}
else if ( option instanceof LockOptions lockOpts ) {
lockOptions = lockOpts;
}
else if ( option instanceof PessimisticLockScope pessimisticLockScope ) {
lockOptions.setLockScope( pessimisticLockScope );
}
else if ( option instanceof Timeout timeout ) {
lockOptions.setTimeOut( timeout.milliseconds() );
}
else if ( option instanceof EnabledFetchProfile enabledFetchProfile ) {
loadAccess.enableFetchProfile( enabledFetchProfile.profileName() );
}
else if ( option instanceof ReadOnlyMode ) {
loadAccess.withReadOnly( option == ReadOnlyMode.READ_ONLY );
}
}
loadAccess.with( lockOptions ).with( interpretCacheMode( storeMode, retrieveMode ) );
return loadAccess;
}
@Override
public <E> List<E> findAll(Class<E> entityType, List<Object> ids) {
return this.byMultipleIds( entityType ).multiLoad( ids );
public <E> List<E> findAll(Class<E> entityType, List<Object> ids, FindOption... options) {
return multiloadAccessWithOptions( entityType, options ).multiLoad( ids );
}
@Override
public <T> T get(Class<T> entityClass, Object id) throws HibernateException {
return this.byId( entityClass ).load( id );
return byId( entityClass ).load( id );
}
@Override
public Object get(String entityName, Object id) throws HibernateException {
return this.byId( entityName ).load( id );
return byId( entityName ).load( id );
}
/**
@ -2360,6 +2402,10 @@ public class SessionImpl
else if ( option instanceof CacheRetrieveMode cacheRetrieveMode ) {
retrieveMode = cacheRetrieveMode;
}
else if ( option instanceof CacheMode cacheMode ) {
storeMode = cacheMode.getJpaStoreMode();
retrieveMode = cacheMode.getJpaRetrieveMode();
}
else if ( option instanceof LockModeType lockModeType ) {
lockOptions.setLockMode( LockModeTypeHelper.getLockMode( lockModeType ) );
}

View File

@ -9,17 +9,27 @@ package org.hibernate.loader.ast.internal;
import org.hibernate.engine.spi.EntityHolder;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.engine.spi.SubselectFetch;
import org.hibernate.query.spi.QueryOptions;
import org.hibernate.sql.exec.internal.BaseExecutionContext;
class ExecutionContextWithSubselectFetchHandler extends BaseExecutionContext {
private final SubselectFetch.RegistrationHandler subSelectFetchableKeysHandler;
private final boolean readOnly;
public ExecutionContextWithSubselectFetchHandler(
SharedSessionContractImplementor session,
SubselectFetch.RegistrationHandler subSelectFetchableKeysHandler) {
this( session, subSelectFetchableKeysHandler, false );
}
public ExecutionContextWithSubselectFetchHandler(
SharedSessionContractImplementor session,
SubselectFetch.RegistrationHandler subSelectFetchableKeysHandler,
boolean readOnly) {
super( session );
this.subSelectFetchableKeysHandler = subSelectFetchableKeysHandler;
this.readOnly = readOnly;
}
@Override
@ -29,4 +39,8 @@ class ExecutionContextWithSubselectFetchHandler extends BaseExecutionContext {
}
}
@Override
public QueryOptions getQueryOptions() {
return readOnly ? QueryOptions.READ_ONLY : super.getQueryOptions();
}
}

View File

@ -43,6 +43,7 @@ import org.hibernate.sql.results.spi.ManagedResultConsumer;
import org.checkerframework.checker.nullness.qual.NonNull;
import static java.lang.Boolean.TRUE;
import static org.hibernate.internal.util.collections.CollectionHelper.isEmpty;
/**
@ -193,7 +194,9 @@ public class MultiIdEntityLoaderArrayParam<E> extends AbstractMultiIdEntityLoade
session.getJdbcServices().getJdbcSelectExecutor().executeQuery(
jdbcSelectOperation,
jdbcParameterBindings,
new ExecutionContextWithSubselectFetchHandler( session, subSelectFetchableKeysHandler ),
new ExecutionContextWithSubselectFetchHandler( session,
subSelectFetchableKeysHandler,
TRUE.equals( loadOptions.getReadOnly(session) ) ),
RowTransformerStandardImpl.instance(),
null,
idsToLoadFromDatabase.size(),

View File

@ -40,6 +40,8 @@ import org.hibernate.sql.results.spi.ListResultsConsumer;
import org.jboss.logging.Logger;
import static java.lang.Boolean.TRUE;
/**
* Standard MultiIdEntityLoader
*
@ -157,7 +159,7 @@ public class MultiIdEntityLoaderStandard<T> extends AbstractMultiIdEntityLoader<
if ( idsInBatch.size() >= maxBatchSize ) {
// we've hit the allotted max-batch-size, perform an "intermediate load"
loadEntitiesById( idsInBatch, lockOptions, session );
loadEntitiesById( idsInBatch, lockOptions, loadOptions, session );
idsInBatch.clear();
}
@ -169,7 +171,7 @@ public class MultiIdEntityLoaderStandard<T> extends AbstractMultiIdEntityLoader<
if ( !idsInBatch.isEmpty() ) {
// we still have ids to load from the processing above since the last max-batch-size trigger,
// perform a load for them
loadEntitiesById( idsInBatch, lockOptions, session );
loadEntitiesById( idsInBatch, lockOptions, loadOptions, session );
}
// for each result where we set the EntityKey earlier, replace them
@ -197,7 +199,8 @@ public class MultiIdEntityLoaderStandard<T> extends AbstractMultiIdEntityLoader<
private List<T> loadEntitiesById(
List<Object> idsInBatch,
LockOptions lockOptions,
SharedSessionContractImplementor session) {
MultiIdLoadOptions loadOptions,
EventSource session) {
assert idsInBatch != null;
assert ! idsInBatch.isEmpty();
@ -265,7 +268,9 @@ public class MultiIdEntityLoaderStandard<T> extends AbstractMultiIdEntityLoader<
return session.getJdbcServices().getJdbcSelectExecutor().list(
jdbcSelect,
jdbcParameterBindings,
new ExecutionContextWithSubselectFetchHandler( session, subSelectFetchableKeysHandler ),
new ExecutionContextWithSubselectFetchHandler( session,
subSelectFetchableKeysHandler,
TRUE.equals( loadOptions.getReadOnly(session) ) ),
RowTransformerStandardImpl.instance(),
null,
ListResultsConsumer.UniqueSemantic.FILTER,
@ -403,7 +408,7 @@ public class MultiIdEntityLoaderStandard<T> extends AbstractMultiIdEntityLoader<
System.arraycopy( ids, idPosition, idsInBatch, 0, batchSize );
result.addAll(
loadEntitiesById( Arrays.asList( idsInBatch ), lockOptions, session )
loadEntitiesById( Arrays.asList( idsInBatch ), lockOptions, loadOptions, session )
);
numberOfIdsLeft = numberOfIdsLeft - batchSize;

View File

@ -6,6 +6,8 @@
*/
package org.hibernate.loader.ast.spi;
import org.hibernate.engine.spi.SessionImplementor;
/**
* Encapsulation of the options for loading multiple entities by id
*/
@ -25,4 +27,9 @@ public interface MultiIdLoadOptions extends MultiLoadOptions {
* @return the session factory cache is checked first
*/
boolean isSecondLevelCacheCheckingEnabled();
/**
* Should the entities be loaded in read-only mode?
*/
Boolean getReadOnly(SessionImplementor session);
}

View File

@ -108,7 +108,7 @@ public class IdentifierLoadAccessImpl<T> implements IdentifierLoadAccess<T>, Jav
final HashSet<String> fetchProfiles =
influencers.adjustFetchProfiles( disabledFetchProfiles, enabledFetchProfiles );
final EffectiveEntityGraph effectiveEntityGraph =
session.getLoadQueryInfluencers().applyEntityGraph( rootGraph, graphSemantic);
influencers.applyEntityGraph( rootGraph, graphSemantic);
try {
return executor.get();
}

View File

@ -0,0 +1,80 @@
package org.hibernate.orm.test.loading.multiLoad;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import org.hibernate.EnabledFetchProfile;
import org.hibernate.Hibernate;
import org.hibernate.annotations.FetchProfile;
import org.hibernate.annotations.FetchProfileOverride;
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.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@SessionFactory
@DomainModel(annotatedClasses = {FindAllFetchProfileTest.Record.class, FindAllFetchProfileTest.Owner.class})
public class FindAllFetchProfileTest {
@Test void test(SessionFactoryScope scope) {
scope.inTransaction(s-> {
Owner gavin = new Owner("gavin");
s.persist(gavin);
s.persist(new Record(123L,gavin,"hello earth"));
s.persist(new Record(456L,gavin,"hello mars"));
});
scope.inTransaction(s-> {
List<Record> all = s.findAll(Record.class, List.of(456L, 123L, 2L));
assertEquals("hello mars",all.get(0).message);
assertEquals("hello earth",all.get(1).message);
assertNull(all.get(2));
assertFalse(Hibernate.isInitialized(all.get(0).owner));
assertFalse(Hibernate.isInitialized(all.get(1).owner));
});
scope.inTransaction(s-> {
List<Record> all = s.findAll(Record.class, List.of(456L, 123L),
new EnabledFetchProfile("withOwner"));
assertEquals("hello mars",all.get(0).message);
assertEquals("hello earth",all.get(1).message);
assertTrue(Hibernate.isInitialized(all.get(0).owner));
assertTrue(Hibernate.isInitialized(all.get(1).owner));
});
}
@Entity
@FetchProfile(name = "withOwner")
static class Record {
@Id Long id;
String message;
@FetchProfileOverride(profile = "withOwner")
@ManyToOne(fetch = FetchType.LAZY)
Owner owner;
Record(Long id, Owner owner, String message) {
this.id = id;
this.owner = owner;
this.message = message;
}
Record() {
}
}
@Entity
static class Owner {
@Id String name;
Owner(String name) {
this.name = name;
}
Owner() {
}
}
}

View File

@ -9,8 +9,10 @@ import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.hibernate.ReadOnlyMode.READ_ONLY;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@SessionFactory
@DomainModel(annotatedClasses = FindAllTest.Record.class)
@ -26,6 +28,13 @@ public class FindAllTest {
assertEquals("hello earth",all.get(1).message);
assertNull(all.get(2));
});
scope.inTransaction(s-> {
List<Record> all = s.findAll(Record.class, List.of(456L, 123L), READ_ONLY);
assertEquals("hello mars",all.get(0).message);
assertEquals("hello earth",all.get(1).message);
assertTrue(s.isReadOnly(all.get(0)));
assertTrue(s.isReadOnly(all.get(1)));
});
}
@Entity
static class Record {

View File

@ -9,8 +9,8 @@ import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
@SessionFactory
@DomainModel(annotatedClasses = GetAllTest.Record.class)