Skip to content

Commit a472765

Browse files
committed
HHH-20231 Handle different result type in native query caching
1 parent aa7a5d0 commit a472765

4 files changed

Lines changed: 154 additions & 64 deletions

File tree

hibernate-core/src/main/java/org/hibernate/query/results/internal/ResultSetMappingSqlSelection.java

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import org.hibernate.engine.spi.SessionFactoryImplementor;
88
import org.hibernate.metamodel.mapping.BasicValuedMapping;
99
import org.hibernate.metamodel.mapping.JdbcMapping;
10-
import org.hibernate.metamodel.mapping.MappingModelExpressible;
10+
import org.hibernate.metamodel.mapping.JdbcMappingContainer;
1111
import org.hibernate.sql.ast.SqlAstWalker;
1212
import org.hibernate.sql.ast.spi.SqlExpressionAccess;
1313
import org.hibernate.sql.ast.spi.SqlSelection;
@@ -24,18 +24,16 @@
2424
*/
2525
public class ResultSetMappingSqlSelection implements SqlSelection, Expression, SqlExpressionAccess {
2626
private final int valuesArrayPosition;
27-
private final BasicValuedMapping valueMapping;
27+
private final JdbcMapping valueMapping;
2828
private final ValueExtractor<?> valueExtractor;
2929

3030
public ResultSetMappingSqlSelection(int valuesArrayPosition, BasicValuedMapping valueMapping) {
31-
this.valuesArrayPosition = valuesArrayPosition;
32-
this.valueMapping = valueMapping;
33-
this.valueExtractor = valueMapping.getJdbcMapping().getJdbcValueExtractor();
31+
this ( valuesArrayPosition, valueMapping.getJdbcMapping() );
3432
}
3533

3634
public ResultSetMappingSqlSelection(int valuesArrayPosition, JdbcMapping jdbcMapping) {
3735
this.valuesArrayPosition = valuesArrayPosition;
38-
this.valueMapping = null;
36+
this.valueMapping = jdbcMapping;
3937
this.valueExtractor = jdbcMapping.getJdbcValueExtractor();
4038
}
4139

@@ -60,7 +58,7 @@ public Expression getExpression() {
6058
}
6159

6260
@Override
63-
public MappingModelExpressible<?> getExpressionType() {
61+
public JdbcMappingContainer getExpressionType() {
6462
return valueMapping;
6563
}
6664

hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcSelectExecutorStandardImpl.java

Lines changed: 75 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ private JdbcValues resolveJdbcValuesSource(
263263

264264
final QueryKey queryResultsCacheKey;
265265
final List<?> cachedResults;
266+
final String queryCacheRegionName;
266267
if ( cacheable && cacheMode.isGetEnabled() ) {
267268
SQL_EXEC_LOGGER.tracef( "Reading query result cache data [%s]", cacheMode.name() );
268269
final Set<String> querySpaces = jdbcSelect.getAffectedTableNames();
@@ -273,9 +274,9 @@ private JdbcValues resolveJdbcValuesSource(
273274
SQL_EXEC_LOGGER.tracef( "Affected query spaces %s", querySpaces );
274275
}
275276

276-
final var queryCache =
277-
factory.getCache()
278-
.getQueryResultsCache( queryOptions.getResultCacheRegionName() );
277+
final var queryCache = factory.getCache()
278+
.getQueryResultsCache( queryOptions.getResultCacheRegionName() );
279+
queryCacheRegionName = queryCache.getRegion().getName();
279280

280281
queryResultsCacheKey = QueryKey.from(
281282
jdbcSelect.getSqlString(),
@@ -299,23 +300,14 @@ private JdbcValues resolveJdbcValuesSource(
299300
//
300301
// todo (6.0) : if we go this route (^^), still beneficial to have an abstraction over different UpdateTimestampsCache-based
301302
// invalidation strategies - QueryCacheInvalidationStrategy
302-
303-
final var statistics = factory.getStatistics();
304-
if ( statistics.isStatisticsEnabled() ) {
305-
if ( cachedResults == null ) {
306-
statistics.queryCacheMiss( queryIdentifier, queryCache.getRegion().getName() );
307-
}
308-
else {
309-
statistics.queryCacheHit( queryIdentifier, queryCache.getRegion().getName() );
310-
}
311-
}
312303
}
313304
else {
314305
SQL_EXEC_LOGGER.tracef( "Skipping reading query result cache data (query cache %s, cache mode %s)",
315306
queryCacheEnabled ? "enabled" : "disabled",
316307
cacheMode.name()
317308
);
318309
cachedResults = null;
310+
queryCacheRegionName = null;
319311
if ( cacheable && cacheMode.isPutEnabled() ) {
320312
queryResultsCacheKey = QueryKey.from(
321313
jdbcSelect.getSqlString(),
@@ -329,7 +321,7 @@ private JdbcValues resolveJdbcValuesSource(
329321
}
330322
}
331323

332-
return resolveJdbcValues(
324+
final var result = resolveJdbcValues(
333325
queryIdentifier,
334326
executionContext,
335327
resultSetAccess,
@@ -339,6 +331,20 @@ private JdbcValues resolveJdbcValuesSource(
339331
session,
340332
factory
341333
);
334+
335+
if ( queryCacheRegionName != null ) {
336+
final var statistics = factory.getStatistics();
337+
if ( statistics.isStatisticsEnabled() ) {
338+
if ( result instanceof JdbcValuesCacheHit ) {
339+
statistics.queryCacheHit( queryIdentifier, queryCacheRegionName );
340+
}
341+
else {
342+
statistics.queryCacheMiss( queryIdentifier, queryCacheRegionName );
343+
}
344+
}
345+
}
346+
347+
return result;
342348
}
343349

344350
private static AbstractJdbcValues resolveJdbcValues(
@@ -351,39 +357,49 @@ private static AbstractJdbcValues resolveJdbcValues(
351357
SharedSessionContractImplementor session,
352358
SessionFactoryImplementor factory) {
353359
final var loadQueryInfluencers = session.getLoadQueryInfluencers();
354-
if ( cachedResults == null ) {
355-
final CachedJdbcValuesMetadata metadataForCache;
356-
final JdbcValuesMapping jdbcValuesMapping;
357-
if ( queryResultsCacheKey == null ) {
358-
jdbcValuesMapping = mappingProducer.resolve( resultSetAccess, loadQueryInfluencers, factory );
359-
metadataForCache = null;
360+
// Try to use cached results if available
361+
if ( cachedResults != null ) {
362+
try {
363+
final var valuesMetadata =
364+
!cachedResults.isEmpty()
365+
&& cachedResults.get( 0 ) instanceof JdbcValuesMetadata jdbcValuesMetadata
366+
? jdbcValuesMetadata
367+
: resultSetAccess;
368+
final var resolvedMapping =
369+
mappingProducer.resolve( valuesMetadata, loadQueryInfluencers, factory );
370+
final var cacheHit = new JdbcValuesCacheHit( cachedResults, resolvedMapping );
371+
if ( cacheHit.isCacheCompatible() ) {
372+
return cacheHit;
373+
}
374+
// Cached data incompatible with the resolved mapping — fall through to re-execute
360375
}
361-
else {
362-
// If we need to put the values into the cache, we need to be able to capture the JdbcValuesMetadata
363-
final var capturingMetadata = new CapturingJdbcValuesMetadata( resultSetAccess );
364-
jdbcValuesMapping = mappingProducer.resolve( capturingMetadata, loadQueryInfluencers, factory );
365-
metadataForCache = capturingMetadata.resolveMetadataForCache();
376+
catch (CachedJdbcValuesMetadata.CacheMetadataIncompleteException e) {
377+
// Cached metadata doesn't cover all required columns — fall through to re-execute
366378
}
367-
return new JdbcValuesResultSetImpl(
368-
resultSetAccess,
369-
queryResultsCacheKey,
370-
queryIdentifier,
371-
executionContext.getQueryOptions(),
372-
resultSetAccess.usesFollowOnLocking(),
373-
jdbcValuesMapping,
374-
metadataForCache,
375-
executionContext
376-
);
379+
}
380+
// Execute query (cache miss or insufficient cached data)
381+
final CachedJdbcValuesMetadata metadataForCache;
382+
final JdbcValuesMapping jdbcValuesMapping;
383+
if ( queryResultsCacheKey == null ) {
384+
jdbcValuesMapping = mappingProducer.resolve( resultSetAccess, loadQueryInfluencers, factory );
385+
metadataForCache = null;
377386
}
378387
else {
379-
final var valuesMetadata =
380-
!cachedResults.isEmpty()
381-
&& cachedResults.get( 0 ) instanceof JdbcValuesMetadata jdbcValuesMetadata
382-
? jdbcValuesMetadata
383-
: resultSetAccess;
384-
return new JdbcValuesCacheHit( cachedResults,
385-
mappingProducer.resolve( valuesMetadata, loadQueryInfluencers, factory ) );
388+
// If we need to put the values into the cache, we need to be able to capture the JdbcValuesMetadata
389+
final var capturingMetadata = new CapturingJdbcValuesMetadata( resultSetAccess );
390+
jdbcValuesMapping = mappingProducer.resolve( capturingMetadata, loadQueryInfluencers, factory );
391+
metadataForCache = capturingMetadata.resolveMetadataForCache( jdbcValuesMapping );
386392
}
393+
return new JdbcValuesResultSetImpl(
394+
resultSetAccess,
395+
queryResultsCacheKey,
396+
queryIdentifier,
397+
executionContext.getQueryOptions(),
398+
resultSetAccess.usesFollowOnLocking(),
399+
jdbcValuesMapping,
400+
metadataForCache,
401+
executionContext
402+
);
387403
}
388404

389405
private static CacheMode resolveCacheMode(ExecutionContext executionContext) {
@@ -467,8 +483,23 @@ public <J> BasicType<J> resolveType(
467483
return basicType;
468484
}
469485

470-
public CachedJdbcValuesMetadata resolveMetadataForCache() {
471-
return columnNames == null ? null : new CachedJdbcValuesMetadata( columnNames, types );
486+
public CachedJdbcValuesMetadata resolveMetadataForCache(JdbcValuesMapping jdbcValuesMapping) {
487+
if ( columnNames == null ) {
488+
return null;
489+
}
490+
// Fill in types from the mapping's SqlSelections for positions that
491+
// were not captured during mapping resolution (e.g. entity results)
492+
for ( var selection : jdbcValuesMapping.getSqlSelections() ) {
493+
final int pos = selection.getValuesArrayPosition();
494+
if ( types[pos] == null && selection.getExpressionType() != null ) {
495+
types[pos] = (BasicType<?>) selection.getExpressionType().getSingleJdbcMapping();
496+
}
497+
}
498+
return new CachedJdbcValuesMetadata(
499+
columnNames,
500+
types,
501+
jdbcValuesMapping.getValueIndexesToCacheIndexes()
502+
);
472503
}
473504
}
474505

hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/CachedJdbcValuesMetadata.java

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
import java.io.Serializable;
88

9-
import org.hibernate.internal.util.collections.ArrayHelper;
109
import org.hibernate.sql.results.jdbc.spi.JdbcValuesMetadata;
1110
import org.hibernate.type.BasicType;
1211
import org.hibernate.type.descriptor.java.JavaType;
@@ -15,10 +14,21 @@
1514
public final class CachedJdbcValuesMetadata implements JdbcValuesMetadata, Serializable {
1615
private final String[] columnNames;
1716
private final BasicType<?>[] types;
17+
private final int[] valueIndexesToCacheIndexes;
1818

19-
public CachedJdbcValuesMetadata(String[] columnNames, BasicType<?>[] types) {
19+
public CachedJdbcValuesMetadata(String[] columnNames, BasicType<?>[] types, int[] valueIndexesToCacheIndexes) {
2020
this.columnNames = columnNames;
2121
this.types = types;
22+
this.valueIndexesToCacheIndexes = valueIndexesToCacheIndexes;
23+
}
24+
25+
public int[] getValueIndexesToCacheIndexes() {
26+
return valueIndexesToCacheIndexes;
27+
}
28+
29+
public JavaType<?> getStoredJavaType(int columnIndex) {
30+
final var type = types[columnIndex];
31+
return type != null ? type.getJavaTypeDescriptor() : null;
2232
}
2333

2434
@Override
@@ -28,18 +38,19 @@ public int getColumnCount() {
2838

2939
@Override
3040
public int resolveColumnPosition(String columnName) {
31-
final int position = ArrayHelper.indexOf( columnNames, columnName ) + 1;
32-
if ( position == 0 ) {
33-
throw new IllegalStateException( "Unexpected resolving of unavailable column: " + columnName );
41+
for ( int i = 0; i < columnNames.length; i++ ) {
42+
if ( columnName.equalsIgnoreCase( columnNames[i] ) ) {
43+
return i + 1;
44+
}
3445
}
35-
return position;
46+
throw new CacheMetadataIncompleteException( "Column unavailable with name: " + columnName );
3647
}
3748

3849
@Override
3950
public String resolveColumnName(int position) {
4051
final String name = columnNames[position - 1];
4152
if ( name == null ) {
42-
throw new IllegalStateException( "Unexpected resolving of unavailable column at position: " + position );
53+
throw new CacheMetadataIncompleteException( "Column unavailable at position: " + position );
4354
}
4455
return name;
4556
}
@@ -51,7 +62,7 @@ public <J> BasicType<J> resolveType(
5162
TypeConfiguration typeConfiguration) {
5263
final BasicType<?> type = types[position - 1];
5364
if ( type == null ) {
54-
throw new IllegalStateException( "Unexpected resolving of unavailable column at position: " + position );
65+
throw new CacheMetadataIncompleteException( "Column unavailable at position: " + position );
5566
}
5667
if ( explicitJavaType == null || type.getJavaTypeDescriptor() == explicitJavaType ) {
5768
//noinspection unchecked
@@ -65,4 +76,13 @@ public <J> BasicType<J> resolveType(
6576
}
6677
}
6778

79+
/**
80+
* Thrown when the cached metadata does not contain information for a requested column
81+
*/
82+
public static class CacheMetadataIncompleteException extends RuntimeException {
83+
public CacheMetadataIncompleteException(String message) {
84+
super( message );
85+
}
86+
}
87+
6888
}

hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/JdbcValuesCacheHit.java

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,61 @@ public class JdbcValuesCacheHit extends AbstractJdbcValues {
2020
private final int numberOfRows;
2121
private final JdbcValuesMapping resolvedMapping;
2222
private final int[] valueIndexesToCacheIndexes;
23+
private final boolean cacheCompatible;
2324
private final int offset;
2425
private final int resultCount;
2526
private int position = -1;
2627

2728
public JdbcValuesCacheHit(List<?> cachedResults, JdbcValuesMapping resolvedMapping) {
2829
// See QueryCachePutManagerEnabledImpl for what is being put into the cached results
2930
this.cachedResults = cachedResults;
30-
this.offset = !cachedResults.isEmpty() && cachedResults.get( 0 ) instanceof CachedJdbcValuesMetadata ? 1 : 0;
31+
final CachedJdbcValuesMetadata metadata = !cachedResults.isEmpty()
32+
&& cachedResults.get( 0 ) instanceof CachedJdbcValuesMetadata cachedMetadata
33+
? cachedMetadata
34+
: null;
35+
this.offset = metadata != null ? 1 : 0;
3136
this.numberOfRows = cachedResults.size() - offset - 1;
3237
this.resultCount = cachedResults.isEmpty() ? 0 : (int) cachedResults.get( cachedResults.size() - 1 );
3338
this.resolvedMapping = resolvedMapping;
34-
this.valueIndexesToCacheIndexes = resolvedMapping.getValueIndexesToCacheIndexes();
39+
if ( metadata != null ) {
40+
this.cacheCompatible = isCacheCompatible( resolvedMapping, metadata );
41+
this.valueIndexesToCacheIndexes = metadata.getValueIndexesToCacheIndexes();
42+
}
43+
else {
44+
this.cacheCompatible = true;
45+
this.valueIndexesToCacheIndexes = resolvedMapping.getValueIndexesToCacheIndexes();
46+
}
47+
}
48+
49+
/**
50+
* Checks whether the cached data is compatible with the reader's mapping,
51+
* by verifying that the Java types of cached values match the reader's expected types.
52+
*
53+
* @param resolvedMapping the current result type's mapping
54+
* @param metadata the cached metadata containing stored Java types
55+
*/
56+
private static boolean isCacheCompatible(
57+
JdbcValuesMapping resolvedMapping,
58+
CachedJdbcValuesMetadata metadata) {
59+
for ( var selection : resolvedMapping.getSqlSelections() ) {
60+
final int valueIndex = selection.getValuesArrayPosition();
61+
final var storedJavaType = metadata.getStoredJavaType( valueIndex );
62+
if ( storedJavaType == null
63+
|| selection.getExpressionType().getSingleJdbcMapping().getJavaTypeDescriptor() != storedJavaType ) {
64+
return false;
65+
}
66+
}
67+
return true;
68+
}
69+
70+
/**
71+
* Returns whether the cached data is compatible with the resolved mapping.
72+
* When {@code false}, the cache entry was populated by a query with a different
73+
* result type that either cached different columns or used incompatible Java types,
74+
* and needs to be re-populated.
75+
*/
76+
public boolean isCacheCompatible() {
77+
return cacheCompatible;
3578
}
3679

3780
@Override
@@ -176,9 +219,7 @@ public Object getCurrentRowValue(int valueIndex) {
176219
}
177220
final Object row = cachedResults.get( position + offset );
178221
if ( row instanceof Object[] array ) {
179-
return valueIndexesToCacheIndexes == null
180-
? array[valueIndex]
181-
: array[valueIndexesToCacheIndexes[valueIndex]];
222+
return array[valueIndexesToCacheIndexes[valueIndex]];
182223
}
183224
else {
184225
assert valueIndexesToCacheIndexes[valueIndex] == 0;

0 commit comments

Comments
 (0)