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(
hydratedObjects == null ? null : Collections.singletonList( hydratedObjects ),
hydratedObjects,
resultSet,
session,
queryParameters.isReadOnly( session )
@ -423,7 +423,7 @@ public abstract class Loader {
final EntityKey keyToRead) throws HibernateException {
final int entitySpan = getEntityPersisters().length;
final List<List<Object>> hydratedObjectsPerRow = entitySpan == 0 ?
final List<Object> nullSeparatedHydratedObjects = entitySpan == 0 ?
null : new ArrayList<>( entitySpan );
Object result = null;
@ -431,26 +431,18 @@ public abstract class Loader {
try {
do {
List<Object> hydratedObjects;
if ( hydratedObjectsPerRow == null ) {
hydratedObjects = null;
}
else {
hydratedObjects = new ArrayList<>( entitySpan );
}
Object loaded = getRowFromResultSet(
resultSet,
session,
queryParameters,
getLockModes( queryParameters.getLockOptions() ),
null,
hydratedObjects,
nullSeparatedHydratedObjects,
loadedKeys,
returnProxies
);
if ( hydratedObjects != null && !hydratedObjects.isEmpty() ) {
hydratedObjectsPerRow.add( hydratedObjects );
}
// Signal that a new row starts. Used in initializeEntitiesAndCollections
nullSeparatedHydratedObjects.add( null );
if ( !keyToRead.equals( loadedKeys[0] ) ) {
throw new AssertionFailure(
String.format(
@ -476,7 +468,7 @@ public abstract class Loader {
}
initializeEntitiesAndCollections(
hydratedObjectsPerRow,
nullSeparatedHydratedObjects,
resultSet,
session,
queryParameters.isReadOnly( session )
@ -996,7 +988,7 @@ public abstract class Loader {
final int entitySpan = getEntityPersisters().length;
final boolean createSubselects = isSubselectLoadingEnabled();
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(
rs,
@ -1005,12 +997,12 @@ public abstract class Loader {
returnProxies,
forcedResultTransformer,
maxRows,
hydratedObjectsPerRow,
nullSeparatedHydratedObjectsPerRow,
subselectResultKeys
);
initializeEntitiesAndCollections(
hydratedObjectsPerRow,
nullSeparatedHydratedObjectsPerRow,
rs,
session,
queryParameters.isReadOnly( session ),
@ -1029,7 +1021,7 @@ public abstract class Loader {
boolean returnProxies,
ResultTransformer forcedResultTransformer,
int maxRows,
List<List<Object>> hydratedObjectsPerRow,
List<Object> nullSeparatedHydratedObjects,
List<EntityKey[]> subselectResultKeys) throws SQLException {
final int entitySpan = getEntityPersisters().length;
final boolean createSubselects = isSubselectLoadingEnabled();
@ -1047,28 +1039,20 @@ public abstract class Loader {
if ( debugEnabled ) {
LOG.debugf( "Result set row: %s", count );
}
List<Object> hydratedObjects;
if ( hydratedObjectsPerRow == null ) {
hydratedObjects = null;
}
else {
hydratedObjects = new ArrayList<>( entitySpan );
}
Object result = getRowFromResultSet(
rs,
session,
queryParameters,
lockModesArray,
optionalObjectKey,
hydratedObjects,
nullSeparatedHydratedObjects,
keys,
returnProxies,
forcedResultTransformer
);
results.add( result );
if ( hydratedObjects != null && !hydratedObjects.isEmpty() ) {
hydratedObjectsPerRow.add( hydratedObjects );
}
// Signal that a new row starts. Used in initializeEntitiesAndCollections
nullSeparatedHydratedObjects.add( null );
if ( createSubselects ) {
subselectResultKeys.add( keys );
keys = new EntityKey[entitySpan]; //can't reuse in this case
@ -1159,12 +1143,12 @@ public abstract class Loader {
}
private void initializeEntitiesAndCollections(
final List<List<Object>> hydratedObjectsPerRow,
final List<Object> nullSeparatedHydratedObjects,
final Object resultSetId,
final SharedSessionContractImplementor session,
final boolean readOnly) throws HibernateException {
initializeEntitiesAndCollections(
hydratedObjectsPerRow,
nullSeparatedHydratedObjects,
resultSetId,
session,
readOnly,
@ -1173,7 +1157,7 @@ public abstract class Loader {
}
private void initializeEntitiesAndCollections(
final List<List<Object>> hydratedObjectsPerRow,
final List<Object> nullSeparatedHydratedObjects,
final Object resultSetId,
final SharedSessionContractImplementor session,
final boolean readOnly,
@ -1205,11 +1189,13 @@ public abstract class Loader {
post = null;
}
if ( hydratedObjectsPerRow != null && !hydratedObjectsPerRow.isEmpty() ) {
if ( nullSeparatedHydratedObjects != null && !nullSeparatedHydratedObjects.isEmpty() ) {
if ( LOG.isTraceEnabled() ) {
int hydratedObjectsSize = 0;
for ( List<Object> hydratedObjects : hydratedObjectsPerRow ) {
hydratedObjectsSize += hydratedObjects.size();
for ( Object hydratedObject : nullSeparatedHydratedObjects ) {
if ( hydratedObject != null ) {
++hydratedObjectsSize;
}
}
LOG.tracev( "Total objects hydrated: {0}", hydratedObjectsSize );
}
@ -1222,17 +1208,21 @@ public abstract class Loader {
.listeners();
GraphImplementor<?> fetchGraphLoadContextToRestore = session.getFetchGraphLoadContext();
for ( List<?> hydratedObjectsForRow : hydratedObjectsPerRow ) {
for ( Object hydratedObject : hydratedObjectsForRow ) {
TwoPhaseLoad.initializeEntity( hydratedObject, readOnly, session, pre, listeners );
for ( Object hydratedObject : nullSeparatedHydratedObjects ) {
if ( hydratedObject == null ) {
// 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.
// 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 );
TwoPhaseLoad.initializeEntity( hydratedObject, readOnly, session, pre, listeners );
}
}
@ -1248,11 +1238,14 @@ public abstract class Loader {
}
}
if ( hydratedObjectsPerRow != null ) {
for ( List<?> hydratedObjectsForRow : hydratedObjectsPerRow ) {
for ( Object hydratedObject : hydratedObjectsForRow ) {
TwoPhaseLoad.afterInitialize( hydratedObject, session );
if ( nullSeparatedHydratedObjects != null ) {
for ( Object hydratedObject : nullSeparatedHydratedObjects ) {
if ( hydratedObject == null ) {
// 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
// persistence context.
final PersistenceContext persistenceContext = session.getPersistenceContextInternal();
if ( hydratedObjectsPerRow != null && !hydratedObjectsPerRow.isEmpty() ) {
for ( List<?> hydratedObjectsForRow : hydratedObjectsPerRow ) {
for ( Object hydratedObject : hydratedObjectsForRow ) {
TwoPhaseLoad.postLoad( hydratedObject, session, post );
if ( afterLoadActions != null ) {
for ( AfterLoadAction afterLoadAction : afterLoadActions ) {
final EntityEntry entityEntry = persistenceContext.getEntry( hydratedObject );
if ( entityEntry == null ) {
// big problem
throw new HibernateException(
"Could not locate EntityEntry immediately after two-phase load"
);
}
afterLoadAction.afterLoad( session, hydratedObject, (Loadable) entityEntry.getPersister() );
if ( nullSeparatedHydratedObjects != null && !nullSeparatedHydratedObjects.isEmpty() ) {
for ( Object hydratedObject : nullSeparatedHydratedObjects ) {
if ( hydratedObject == null ) {
// This is a hack to signal that we're starting to process a new row
// Ignore
continue;
}
TwoPhaseLoad.postLoad( hydratedObject, session, post );
if ( afterLoadActions != null ) {
for ( AfterLoadAction afterLoadAction : afterLoadActions ) {
final EntityEntry entityEntry = persistenceContext.getEntry( hydratedObject );
if ( entityEntry == null ) {
// big problem
throw new HibernateException(
"Could not locate EntityEntry immediately after two-phase load"
);
}
afterLoadAction.afterLoad( session, hydratedObject, (Loadable) entityEntry.getPersister() );
}
}
}