HHH-14124 Avoid too many list allocations when hydrating query results

I suspect the original goal of having a single list of hydrated objects
for all rows was to avoid instantiating one list per row, but we did
just that in the fix in the last commit.

This introduces a hack to signal that we're starting to process a new
row while still keeping a flat, single-list structure: we inserting
null elements in the list of hydrated objects.

This is admittedly very ugly, but it's the only solution I can come up
with if we want to keep the number of memory allocations similar to what
we used to have. And hopefully this code will disappear in ORM 6.0.
This commit is contained in:
Yoann Rodière 2020-07-29 13:23:52 +02:00 committed by Andrea Boriero
parent 3bdc5af63d
commit 2c98ab236d
1 changed files with 58 additions and 62 deletions

View File

@ -406,7 +406,7 @@ public abstract class Loader {
} }
initializeEntitiesAndCollections( initializeEntitiesAndCollections(
hydratedObjects == null ? null : Collections.singletonList( hydratedObjects ), hydratedObjects,
resultSet, resultSet,
session, session,
queryParameters.isReadOnly( session ) queryParameters.isReadOnly( session )
@ -423,7 +423,7 @@ public abstract class Loader {
final EntityKey keyToRead) throws HibernateException { final EntityKey keyToRead) throws HibernateException {
final int entitySpan = getEntityPersisters().length; final int entitySpan = getEntityPersisters().length;
final List<List<Object>> hydratedObjectsPerRow = entitySpan == 0 ? final List<Object> nullSeparatedHydratedObjects = entitySpan == 0 ?
null : new ArrayList<>( entitySpan ); null : new ArrayList<>( entitySpan );
Object result = null; Object result = null;
@ -431,26 +431,18 @@ public abstract class Loader {
try { try {
do { do {
List<Object> hydratedObjects;
if ( hydratedObjectsPerRow == null ) {
hydratedObjects = null;
}
else {
hydratedObjects = new ArrayList<>( entitySpan );
}
Object loaded = getRowFromResultSet( Object loaded = getRowFromResultSet(
resultSet, resultSet,
session, session,
queryParameters, queryParameters,
getLockModes( queryParameters.getLockOptions() ), getLockModes( queryParameters.getLockOptions() ),
null, null,
hydratedObjects, nullSeparatedHydratedObjects,
loadedKeys, loadedKeys,
returnProxies returnProxies
); );
if ( hydratedObjects != null && !hydratedObjects.isEmpty() ) { // Signal that a new row starts. Used in initializeEntitiesAndCollections
hydratedObjectsPerRow.add( hydratedObjects ); nullSeparatedHydratedObjects.add( null );
}
if ( !keyToRead.equals( loadedKeys[0] ) ) { if ( !keyToRead.equals( loadedKeys[0] ) ) {
throw new AssertionFailure( throw new AssertionFailure(
String.format( String.format(
@ -476,7 +468,7 @@ public abstract class Loader {
} }
initializeEntitiesAndCollections( initializeEntitiesAndCollections(
hydratedObjectsPerRow, nullSeparatedHydratedObjects,
resultSet, resultSet,
session, session,
queryParameters.isReadOnly( session ) queryParameters.isReadOnly( session )
@ -996,7 +988,7 @@ public abstract class Loader {
final int entitySpan = getEntityPersisters().length; final int entitySpan = getEntityPersisters().length;
final boolean createSubselects = isSubselectLoadingEnabled(); final boolean createSubselects = isSubselectLoadingEnabled();
final List<EntityKey[]> subselectResultKeys = createSubselects ? new ArrayList<>() : null; final List<EntityKey[]> subselectResultKeys = createSubselects ? new ArrayList<>() : null;
final List<List<Object>> hydratedObjectsPerRow = entitySpan == 0 ? null : new ArrayList<>(); final List<Object> nullSeparatedHydratedObjectsPerRow = entitySpan == 0 ? null : new ArrayList<>();
final List results = getRowsFromResultSet( final List results = getRowsFromResultSet(
rs, rs,
@ -1005,12 +997,12 @@ public abstract class Loader {
returnProxies, returnProxies,
forcedResultTransformer, forcedResultTransformer,
maxRows, maxRows,
hydratedObjectsPerRow, nullSeparatedHydratedObjectsPerRow,
subselectResultKeys subselectResultKeys
); );
initializeEntitiesAndCollections( initializeEntitiesAndCollections(
hydratedObjectsPerRow, nullSeparatedHydratedObjectsPerRow,
rs, rs,
session, session,
queryParameters.isReadOnly( session ), queryParameters.isReadOnly( session ),
@ -1029,7 +1021,7 @@ public abstract class Loader {
boolean returnProxies, boolean returnProxies,
ResultTransformer forcedResultTransformer, ResultTransformer forcedResultTransformer,
int maxRows, int maxRows,
List<List<Object>> hydratedObjectsPerRow, List<Object> nullSeparatedHydratedObjects,
List<EntityKey[]> subselectResultKeys) throws SQLException { List<EntityKey[]> subselectResultKeys) throws SQLException {
final int entitySpan = getEntityPersisters().length; final int entitySpan = getEntityPersisters().length;
final boolean createSubselects = isSubselectLoadingEnabled(); final boolean createSubselects = isSubselectLoadingEnabled();
@ -1047,28 +1039,20 @@ public abstract class Loader {
if ( debugEnabled ) { if ( debugEnabled ) {
LOG.debugf( "Result set row: %s", count ); LOG.debugf( "Result set row: %s", count );
} }
List<Object> hydratedObjects;
if ( hydratedObjectsPerRow == null ) {
hydratedObjects = null;
}
else {
hydratedObjects = new ArrayList<>( entitySpan );
}
Object result = getRowFromResultSet( Object result = getRowFromResultSet(
rs, rs,
session, session,
queryParameters, queryParameters,
lockModesArray, lockModesArray,
optionalObjectKey, optionalObjectKey,
hydratedObjects, nullSeparatedHydratedObjects,
keys, keys,
returnProxies, returnProxies,
forcedResultTransformer forcedResultTransformer
); );
results.add( result ); results.add( result );
if ( hydratedObjects != null && !hydratedObjects.isEmpty() ) { // Signal that a new row starts. Used in initializeEntitiesAndCollections
hydratedObjectsPerRow.add( hydratedObjects ); nullSeparatedHydratedObjects.add( null );
}
if ( createSubselects ) { if ( createSubselects ) {
subselectResultKeys.add( keys ); subselectResultKeys.add( keys );
keys = new EntityKey[entitySpan]; //can't reuse in this case keys = new EntityKey[entitySpan]; //can't reuse in this case
@ -1159,12 +1143,12 @@ public abstract class Loader {
} }
private void initializeEntitiesAndCollections( private void initializeEntitiesAndCollections(
final List<List<Object>> hydratedObjectsPerRow, final List<Object> nullSeparatedHydratedObjects,
final Object resultSetId, final Object resultSetId,
final SharedSessionContractImplementor session, final SharedSessionContractImplementor session,
final boolean readOnly) throws HibernateException { final boolean readOnly) throws HibernateException {
initializeEntitiesAndCollections( initializeEntitiesAndCollections(
hydratedObjectsPerRow, nullSeparatedHydratedObjects,
resultSetId, resultSetId,
session, session,
readOnly, readOnly,
@ -1173,7 +1157,7 @@ public abstract class Loader {
} }
private void initializeEntitiesAndCollections( private void initializeEntitiesAndCollections(
final List<List<Object>> hydratedObjectsPerRow, final List<Object> nullSeparatedHydratedObjects,
final Object resultSetId, final Object resultSetId,
final SharedSessionContractImplementor session, final SharedSessionContractImplementor session,
final boolean readOnly, final boolean readOnly,
@ -1205,11 +1189,13 @@ public abstract class Loader {
post = null; post = null;
} }
if ( hydratedObjectsPerRow != null && !hydratedObjectsPerRow.isEmpty() ) { if ( nullSeparatedHydratedObjects != null && !nullSeparatedHydratedObjects.isEmpty() ) {
if ( LOG.isTraceEnabled() ) { if ( LOG.isTraceEnabled() ) {
int hydratedObjectsSize = 0; int hydratedObjectsSize = 0;
for ( List<Object> hydratedObjects : hydratedObjectsPerRow ) { for ( Object hydratedObject : nullSeparatedHydratedObjects ) {
hydratedObjectsSize += hydratedObjects.size(); if ( hydratedObject != null ) {
++hydratedObjectsSize;
}
} }
LOG.tracev( "Total objects hydrated: {0}", hydratedObjectsSize ); LOG.tracev( "Total objects hydrated: {0}", hydratedObjectsSize );
} }
@ -1222,17 +1208,21 @@ public abstract class Loader {
.listeners(); .listeners();
GraphImplementor<?> fetchGraphLoadContextToRestore = session.getFetchGraphLoadContext(); GraphImplementor<?> fetchGraphLoadContextToRestore = session.getFetchGraphLoadContext();
for ( List<?> hydratedObjectsForRow : hydratedObjectsPerRow ) { for ( Object hydratedObject : nullSeparatedHydratedObjects ) {
for ( Object hydratedObject : hydratedObjectsForRow ) { if ( hydratedObject == null ) {
TwoPhaseLoad.initializeEntity( hydratedObject, readOnly, session, pre, listeners ); // This is a hack to signal that we're starting to process a new row
// HHH-14124: TwoPhaseLoad has nasty side-effects in order to handle sub-graphs.
// That's very fragile, but someone would need to spend much more time on this
// in order to implement it correctly, and apparently that's already been done in ORM 6.0.
// So for now, we'll just ensure side-effects (and whatever bugs they lead to)
// are limited to each row.
session.setFetchGraphLoadContext( fetchGraphLoadContextToRestore );
continue;
} }
// HHH-14124: TwoPhaseLoad has nasty side-effects in order to handle sub-graphs. TwoPhaseLoad.initializeEntity( hydratedObject, readOnly, session, pre, listeners );
// That's very fragile, but someone would need to spend much more time on this
// in order to implement it correctly, and apparently that's already been done in ORM 6.0.
// So for now, we'll just ensure side-effects (and whatever bugs they lead to)
// are limited to each row.
session.setFetchGraphLoadContext( fetchGraphLoadContextToRestore );
} }
} }
@ -1248,11 +1238,14 @@ public abstract class Loader {
} }
} }
if ( hydratedObjectsPerRow != null ) { if ( nullSeparatedHydratedObjects != null ) {
for ( List<?> hydratedObjectsForRow : hydratedObjectsPerRow ) { for ( Object hydratedObject : nullSeparatedHydratedObjects ) {
for ( Object hydratedObject : hydratedObjectsForRow ) { if ( hydratedObject == null ) {
TwoPhaseLoad.afterInitialize( hydratedObject, session ); // This is a hack to signal that we're starting to process a new row
// Ignore
continue;
} }
TwoPhaseLoad.afterInitialize( hydratedObject, session );
} }
} }
@ -1261,21 +1254,24 @@ public abstract class Loader {
// endCollectionLoad to ensure the collection is in the // endCollectionLoad to ensure the collection is in the
// persistence context. // persistence context.
final PersistenceContext persistenceContext = session.getPersistenceContextInternal(); final PersistenceContext persistenceContext = session.getPersistenceContextInternal();
if ( hydratedObjectsPerRow != null && !hydratedObjectsPerRow.isEmpty() ) { if ( nullSeparatedHydratedObjects != null && !nullSeparatedHydratedObjects.isEmpty() ) {
for ( List<?> hydratedObjectsForRow : hydratedObjectsPerRow ) { for ( Object hydratedObject : nullSeparatedHydratedObjects ) {
for ( Object hydratedObject : hydratedObjectsForRow ) { if ( hydratedObject == null ) {
TwoPhaseLoad.postLoad( hydratedObject, session, post ); // This is a hack to signal that we're starting to process a new row
if ( afterLoadActions != null ) { // Ignore
for ( AfterLoadAction afterLoadAction : afterLoadActions ) { continue;
final EntityEntry entityEntry = persistenceContext.getEntry( hydratedObject ); }
if ( entityEntry == null ) { TwoPhaseLoad.postLoad( hydratedObject, session, post );
// big problem if ( afterLoadActions != null ) {
throw new HibernateException( for ( AfterLoadAction afterLoadAction : afterLoadActions ) {
"Could not locate EntityEntry immediately after two-phase load" final EntityEntry entityEntry = persistenceContext.getEntry( hydratedObject );
); if ( entityEntry == null ) {
} // big problem
afterLoadAction.afterLoad( session, hydratedObject, (Loadable) entityEntry.getPersister() ); throw new HibernateException(
"Could not locate EntityEntry immediately after two-phase load"
);
} }
afterLoadAction.afterLoad( session, hydratedObject, (Loadable) entityEntry.getPersister() );
} }
} }
} }