HHH-16968 StackOverflowError when using NaturalIdLoadAccess and bi-directional association

This commit is contained in:
Andrea Boriero 2023-08-07 11:52:34 +02:00 committed by Andrea Boriero
parent 5d6766698b
commit 81fe7688c4
2 changed files with 105 additions and 53 deletions

View File

@ -6,7 +6,6 @@
*/
package org.hibernate.loader.ast.internal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
@ -51,14 +50,12 @@ import org.hibernate.sql.exec.spi.JdbcParameterBinding;
import org.hibernate.sql.exec.spi.JdbcParameterBindings;
import org.hibernate.sql.exec.spi.JdbcParametersList;
import org.hibernate.sql.results.graph.DomainResult;
import org.hibernate.sql.results.graph.Fetch;
import org.hibernate.sql.results.graph.FetchParent;
import org.hibernate.sql.results.graph.Fetchable;
import org.hibernate.sql.results.graph.FetchableContainer;
import org.hibernate.sql.results.graph.internal.ImmutableFetchList;
import org.hibernate.sql.results.spi.ListResultsConsumer;
import org.hibernate.stat.spi.StatisticsImplementor;
import static java.util.Collections.emptyList;
/**
* Base support for NaturalIdLoader implementations
*/
@ -92,35 +89,82 @@ public abstract class AbstractNaturalIdLoader<T> implements NaturalIdLoader<T> {
@Override
public T load(Object naturalIdValue, NaturalIdLoadOptions options, SharedSessionContractImplementor session) {
return selectByNaturalId(
naturalIdMapping().normalizeInput( naturalIdValue ),
options,
(tableGroup, creationState) -> entityDescriptor.createDomainResult(
new NavigablePath( entityDescriptor().getRootPathName() ),
tableGroup,
final SessionFactoryImplementor sessionFactory = session.getFactory();
final LockOptions lockOptions = options.getLockOptions() == null ? LockOptions.NONE : options.getLockOptions();
final SelectStatement sqlSelect = LoaderSelectBuilder.createSelect(
getLoadable(),
// null here means to select everything
null,
creationState
true,
emptyList(), // we're going to add the restrictions ourselves
null,
1,
session.getLoadQueryInfluencers(),
lockOptions,
JdbcParametersList.newBuilder()::add,
sessionFactory
);
// we have to add the restrictions ourselves manually because we want special null handling
final JdbcParameterBindings jdbcParamBindings = new JdbcParameterBindingsImpl( naturalIdMapping.getJdbcTypeCount() );
applyNaturalIdRestriction(
naturalIdMapping().normalizeInput( naturalIdValue ),
sqlSelect.getQuerySpec().getFromClause().getRoots().get(0),
sqlSelect.getQuerySpec()::applyPredicate,
jdbcParamBindings::addBinding,
new LoaderSqlAstCreationState(
sqlSelect.getQuerySpec(),
new SqlAliasBaseManager(),
new SimpleFromClauseAccessImpl(),
lockOptions,
(fetchParent, creationState) -> ImmutableFetchList.EMPTY,
true,
new LoadQueryInfluencers( sessionFactory ),
sessionFactory
),
AbstractNaturalIdLoader::visitFetches,
(statsEnabled) -> {
// entityDescriptor().getPreLoadListener().startingLoad( entityDescriptor, naturalIdValue, KeyType.NATURAL_ID, LoadSource.DATABASE );
return statsEnabled ? System.nanoTime() : -1;
},
(result,startToken) -> {
session
);
final QueryOptions queryOptions = new SimpleQueryOptions( lockOptions, false );
final JdbcOperationQuerySelect jdbcSelect =
sessionFactory.getJdbcServices().getJdbcEnvironment().getSqlAstTranslatorFactory()
.buildSelectTranslator( sessionFactory, sqlSelect )
.translate( jdbcParamBindings, queryOptions );
final long startToken = sessionFactory.getStatistics().isStatisticsEnabled() ? System.nanoTime() : -1;
//noinspection unchecked
final List<T> results = session.getFactory().getJdbcServices().getJdbcSelectExecutor().list(
jdbcSelect,
jdbcParamBindings,
new NaturalIdLoaderWithOptionsExecutionContext( session, queryOptions ),
row -> (T) row[0],
ListResultsConsumer.UniqueSemantic.FILTER
);
if ( results.size() > 1 ) {
throw new HibernateException(
String.format(
"Loading by natural-id returned more that one row : %s",
entityDescriptor.getEntityName()
)
);
}
final T result = results.isEmpty() ? null : results.get(0);
// entityDescriptor().getPostLoadListener().completedLoad( result, entityDescriptor(), naturalIdValue, KeyType.NATURAL_ID, LoadSource.DATABASE );
if ( startToken > 0 ) {
session.getFactory().getStatistics().naturalIdQueryExecuted(
entityDescriptor().getEntityPersister().getRootEntityName(),
System.nanoTime() - startToken
);
// // todo (6.0) : need a "load-by-natural-id" stat
// // e.g.,
// // todo (6.0) : need a "load-by-natural-id" stat, e.g.,
// // final Object identifier = entityDescriptor().getIdentifierMapping().getIdentifier( result, session );
// // session.getFactory().getStatistics().entityLoadedByNaturalId( entityDescriptor(), identifier );
}
},
session
);
return result;
}
/**
@ -272,7 +316,7 @@ public abstract class AbstractNaturalIdLoader<T> implements NaturalIdLoader<T> {
null,
creationState
),
AbstractNaturalIdLoader::visitFetches,
(fetchParent, creationState) -> ImmutableFetchList.EMPTY,
(statsEnabled) -> {
// entityDescriptor().getPreLoadListener().startingLoad( entityDescriptor, naturalIdValue, KeyType.NATURAL_ID, LoadSource.DATABASE );
return statsEnabled ? System.nanoTime() : -1L;
@ -357,28 +401,6 @@ public abstract class AbstractNaturalIdLoader<T> implements NaturalIdLoader<T> {
return results.get( 0 );
}
private static ImmutableFetchList visitFetches(
FetchParent fetchParent,
LoaderSqlAstCreationState creationState) {
final FetchableContainer fetchableContainer = fetchParent.getReferencedMappingContainer();
final int size = fetchableContainer.getNumberOfFetchables();
final ImmutableFetchList.Builder fetches = new ImmutableFetchList.Builder( fetchableContainer );
for ( int i = 0; i < size; i++ ) {
final Fetchable fetchable = fetchableContainer.getFetchable( i );
final NavigablePath navigablePath = fetchParent.resolveNavigablePath( fetchable );
final Fetch fetch = fetchParent.generateFetchableFetch(
fetchable,
navigablePath,
fetchable.getMappedFetchOptions().getTiming(),
true,
null,
creationState
);
fetches.add( fetch );
}
return fetches.build();
}
private static class NaturalIdLoaderWithOptionsExecutionContext extends BaseExecutionContext {
private final Callback callback;
private final QueryOptions queryOptions;

View File

@ -281,6 +281,36 @@ public class LoaderSelectBuilder {
return process.generateSelect();
}
// TODO: this method is probably unnecessary if we make
// determineWhetherToForceIdSelection() a bit smarter
static SelectStatement createSelect(
Loadable loadable,
List<ModelPart> partsToSelect,
boolean forceIdentifierSelection,
List<ModelPart> restrictedParts,
DomainResult<?> cachedDomainResult,
int numberOfKeysToLoad,
LoadQueryInfluencers loadQueryInfluencers,
LockOptions lockOptions,
Consumer<JdbcParameter> jdbcParameterConsumer,
SessionFactoryImplementor sessionFactory) {
final LoaderSelectBuilder process = new LoaderSelectBuilder(
sessionFactory,
loadable,
partsToSelect,
restrictedParts,
cachedDomainResult,
numberOfKeysToLoad,
loadQueryInfluencers,
lockOptions,
determineGraphTraversalState( loadQueryInfluencers ),
forceIdentifierSelection,
jdbcParameterConsumer
);
return process.generateSelect();
}
/**
* Create an SQL AST select-statement used for subselect-based CollectionLoader
*