From 71ba9240404cf05905732b1eddc18c7eb131dd0c Mon Sep 17 00:00:00 2001 From: Sanne Grinovero Date: Wed, 21 Oct 2020 14:28:26 +0100 Subject: [PATCH] HHH-14271 Lazy initialization of UniqueEntityLoader for most LockMode types --- .../org/hibernate/cfg/AvailableSettings.java | 15 ++-- .../entity/AbstractEntityPersister.java | 90 ++++++++----------- .../entity/EntityLoaderLazyCollection.java | 45 ++++++++++ 3 files changed, 94 insertions(+), 56 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/persister/entity/EntityLoaderLazyCollection.java diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/AvailableSettings.java b/hibernate-core/src/main/java/org/hibernate/cfg/AvailableSettings.java index 096b563196..231f65d20c 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/AvailableSettings.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/AvailableSettings.java @@ -1652,12 +1652,17 @@ public interface AvailableSettings extends org.hibernate.jpa.AvailableSettings { /** * Controls how the individual Loaders for an entity are created. * - * When `true` (the default), only the minimal set of Loaders are - * created. These include the handling for {@link org.hibernate.LockMode#READ} - * and {@link org.hibernate.LockMode#NONE} as well as specialized Loaders for - * merge and refresh handling. + * When `true` (the default), the loaders are only created on first + * access; this ensures that all access patterns which are not useful + * to the application are never instantiated, possibly saving a + * substantial amount of memory for applications having many entities. + * The only exception is the loader for LockMode.NONE, + * which will always be eagerly initialized; this is necessary to + * detect mapping errors. * - * `false` indicates that all loaders should be created up front + * `false` indicates that all loaders should be created up front; this + * will consume more memory but ensures all necessary memory is + * allocated right away. * * @since 5.3 */ diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java index bfb4fc4d7f..6d3a6231e0 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java @@ -14,7 +14,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.Comparator; -import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -23,7 +22,6 @@ import java.util.Locale; import java.util.Map; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import org.hibernate.AssertionFailure; import org.hibernate.EntityMode; @@ -69,6 +67,7 @@ import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.engine.spi.CachedNaturalIdValueSource; import org.hibernate.engine.spi.CascadeStyle; +import org.hibernate.engine.spi.CascadingAction; import org.hibernate.engine.spi.CascadingActions; import org.hibernate.engine.spi.CollectionKey; import org.hibernate.engine.spi.EntityEntry; @@ -254,9 +253,7 @@ public abstract class AbstractEntityPersister private final LockModeEnumMap lockers = new LockModeEnumMap<>(); - private UniqueEntityLoader noneLockLoader; - private UniqueEntityLoader readLockLoader; - private final Map loaders = new ConcurrentHashMap<>(); + private final EntityLoaderLazyCollection loaders = new EntityLoaderLazyCollection(); // SQL strings private String sqlVersionSelectString; @@ -4293,61 +4290,38 @@ public final void postInstantiate() throws MappingException { protected void doPostInstantiate() { } - /** - * "Needed" by subclasses to override the createLoader strategy - * - * @deprecated Because there are better patterns for this - */ - @Deprecated - protected Map getLoaders() { - return loaders; - } - //Relational based Persisters should be content with this implementation protected void createLoaders() { - // We load the entity loaders for the most common lock modes. - - noneLockLoader = createEntityLoader( LockMode.NONE ); - readLockLoader = createEntityLoader( LockMode.READ ); - - - // The loaders for the other lock modes are lazily loaded and will later be stored in this map, - // unless this setting is disabled + // The loaders for each lock mode are lazily loaded, unless this setting is disabled if ( ! factory.getSessionFactoryOptions().isDelayBatchFetchLoaderCreationsEnabled() ) { - for ( LockMode lockMode : EnumSet.complementOf( EnumSet.of( LockMode.NONE, LockMode.READ, LockMode.WRITE ) ) ) { - loaders.put( lockMode, createEntityLoader( lockMode ) ); + for ( LockMode lockMode : LockMode.values() ) { + //Trigger eager initialization + loaders.getOrBuildByLockMode( lockMode, this::createEntityLoader ); } + //Also, we have two special internal fetch profiles to eagerly initialize in this case: + loaders.getOrCreateByInternalFetchProfileMerge( this::buildMergeCascadeEntityLoader ); + loaders.getOrCreateByInternalFetchProfileRefresh( this::buildRefreshCascadeEntityLoader ); } + else { + //At least initialize this one: it's almost certain to be used, + //and also will allow to report mapping errors during initialization. + loaders.getOrBuildByLockMode( LockMode.NONE, this::createEntityLoader ); + } + } + private UniqueEntityLoader buildMergeCascadeEntityLoader(LockMode ignored) { + return new CascadeEntityLoader( this, CascadingActions.MERGE, getFactory() ); + } - // And finally, create the internal merge and refresh load plans - - loaders.put( - "merge", - new CascadeEntityLoader( this, CascadingActions.MERGE, getFactory() ) - ); - loaders.put( - "refresh", - new CascadeEntityLoader( this, CascadingActions.REFRESH, getFactory() ) - ); + private UniqueEntityLoader buildRefreshCascadeEntityLoader(LockMode ignored) { + return new CascadeEntityLoader( this, CascadingActions.REFRESH, getFactory() ); } protected final UniqueEntityLoader getLoaderByLockMode(LockMode lockMode) { - if ( LockMode.NONE == lockMode ) { - return noneLockLoader; - } - else if ( LockMode.READ == lockMode ) { - return readLockLoader; - } - - return loaders.computeIfAbsent( lockMode, this::generateDelayedEntityLoader ); + return loaders.getOrBuildByLockMode( lockMode, this::createEntityLoader ); } - private UniqueEntityLoader generateDelayedEntityLoader(Object lockModeObject) { - // Unfortunately, the loaders map mixes LockModes and Strings as keys so we need to accept an Object. - // The cast is safe as we will always call this method with a LockMode. - LockMode lockMode = (LockMode) lockModeObject; - + private UniqueEntityLoader generateDelayedEntityLoader(final LockMode lockMode) { switch ( lockMode ) { case NONE: case READ: @@ -4367,7 +4341,7 @@ private UniqueEntityLoader generateDelayedEntityLoader(Object lockModeObject) { && hasSubclasses() && !getFactory().getDialect().supportsOuterJoinForUpdate(); - return disableForUpdate ? readLockLoader : createEntityLoader( lockMode ); + return disableForUpdate ? getLoaderByLockMode( LockMode.READ ) : createEntityLoader( lockMode ); } default: { throw new IllegalStateException( String.format( Locale.ROOT, "Lock mode %1$s not supported by entity loaders.", lockMode ) ); @@ -4433,7 +4407,7 @@ public Object initializeEnhancedEntityUsedAsProxy( loaded = CacheEntityLoaderHelper.INSTANCE.loadFromSecondLevelCache( loadEvent, this, entityKey ); } if ( loaded == null ) { - loaded = readLockLoader.load( + loaded = getLoaderByLockMode( LockMode.READ ).load( identifier, entity, session, @@ -4527,7 +4501,8 @@ else if ( session.getLoadQueryInfluencers().getInternalFetchProfile() != null && // Next, we consider whether an 'internal' fetch profile has been set. // This indicates a special fetch profile Hibernate needs applied // (for its merge loading process e.g.). - return loaders.get( session.getLoadQueryInfluencers().getInternalFetchProfile() ); + final String internalFetchProfile = session.getLoadQueryInfluencers().getInternalFetchProfile(); + return getLoaderByString( internalFetchProfile ); } else if ( isAffectedByEnabledFetchProfiles( session ) ) { // If the session has associated influencers we need to adjust the @@ -4545,6 +4520,19 @@ else if ( lockOptions.getTimeOut() != LockOptions.WAIT_FOREVER ) { } } + private UniqueEntityLoader getLoaderByString(String internalFetchProfile) { + if ( "merge".equals( internalFetchProfile ) ) { + return loaders.getOrCreateByInternalFetchProfileMerge( this::buildMergeCascadeEntityLoader ); + } + else if ( "refresh".equals( internalFetchProfile ) ) { + return loaders.getOrCreateByInternalFetchProfileRefresh( this::buildRefreshCascadeEntityLoader ); + } + else { + //At this time there's no code storing any other fetch profiles; also, the map implementation isn't supporting the option. + return null; + } + } + public final boolean isAllNull(Object[] array, int tableNumber) { for ( int i = 0; i < array.length; i++ ) { if ( isPropertyOfTable( i, tableNumber ) && array[i] != null ) { diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/EntityLoaderLazyCollection.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/EntityLoaderLazyCollection.java new file mode 100644 index 0000000000..336f1f2a25 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/EntityLoaderLazyCollection.java @@ -0,0 +1,45 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.persister.entity; + +import java.util.function.Function; + +import org.hibernate.LockMode; +import org.hibernate.internal.util.collections.LazyIndexedMap; +import org.hibernate.loader.entity.UniqueEntityLoader; + +final class EntityLoaderLazyCollection extends LazyIndexedMap { + + /** + * The need here is weird: we need to store an instance of UniqueEntityLoader + * for each value of the LockMode enum, but also store two special internal + * fetch profiles called "merge" and "refresh". + * We assign these two their own ordinal ids such as to be treated as two + * additional enum states; but to access these they will have their own + * dedicated method implementations. + */ + private static final int MERGE_INDEX = LockMode.values().length; + private static final int REFRESH_INDEX = MERGE_INDEX + 1; + private static final int TOTAL_STORAGE_SIZE = REFRESH_INDEX + 1; + + public EntityLoaderLazyCollection() { + super( TOTAL_STORAGE_SIZE ); + } + + UniqueEntityLoader getOrBuildByLockMode(LockMode lockMode, Function builderFunction) { + return super.computeIfAbsent( lockMode.ordinal(), lockMode, builderFunction ); + } + + UniqueEntityLoader getOrCreateByInternalFetchProfileMerge(Function builderFunction) { + return super.computeIfAbsent( MERGE_INDEX, null, builderFunction ); + } + + UniqueEntityLoader getOrCreateByInternalFetchProfileRefresh(Function builderFunction) { + return super.computeIfAbsent( REFRESH_INDEX, null, builderFunction ); + } + +}