|
47 | 47 | import org.apache.cayenne.query.Query; |
48 | 48 | import org.apache.cayenne.query.QueryCacheStrategy; |
49 | 49 | import org.apache.cayenne.query.QueryMetadata; |
| 50 | +import org.apache.cayenne.query.QueryMetadataProxy; |
50 | 51 | import org.apache.cayenne.query.QueryRouter; |
51 | 52 | import org.apache.cayenne.query.RefreshQuery; |
52 | 53 | import org.apache.cayenne.query.RelationshipQuery; |
@@ -97,7 +98,10 @@ class DataDomainQueryAction implements QueryRouter, OperationObserver { |
97 | 98 | private Map<CayennePath, List<?>> prefetchResultsByPath; |
98 | 99 | private Map<QueryEngine, Collection<Query>> queriesByNode; |
99 | 100 | private boolean noObjectConversion; |
| 101 | + // True when using a caching strategy (shared or local cache), indicating lists are immutable and need copying |
100 | 102 | private boolean cachedResult; |
| 103 | + // True when results were found in cache (cache hit), false when fetched from database (cache miss or explicit refresh) |
| 104 | + private boolean cacheHit; |
101 | 105 |
|
102 | 106 | /* |
103 | 107 | * A constructor for the "new" way of performing a query via 'execute' with |
@@ -450,18 +454,24 @@ private boolean interceptSharedCache() { |
450 | 454 |
|
451 | 455 | // response may already be initialized by the factory above ... |
452 | 456 | // it is null if there was a preexisting cache entry |
| 457 | + cacheHit = (response == null); |
| 458 | + |
453 | 459 | if (response == null || wasResponseNull) { |
454 | 460 | response = new ListResponse(cachedResults); |
455 | 461 | } |
| 462 | + |
| 463 | + // Mark as cached result - lists need copying whether hit or miss |
| 464 | + cachedResult = true; |
456 | 465 |
|
457 | 466 | if (cachedResults instanceof ListWithPrefetches) { |
458 | 467 | this.prefetchResultsByPath = ((ListWithPrefetches) cachedResults).getPrefetchResultsByPath(); |
459 | 468 | } |
460 | 469 | } else { |
461 | 470 | // on cache-refresh request, fetch without blocking and fill the cache |
462 | 471 | queryCache.put(metadata, factory.createObject()); |
| 472 | + cachedResult = true; // Still a cached path, lists need copying |
| 473 | + cacheHit = false; // Not a cache hit, we're refreshing |
463 | 474 | } |
464 | | - cachedResult = true; |
465 | 475 |
|
466 | 476 | return DONE; |
467 | 477 | } |
@@ -723,14 +733,28 @@ protected PrefetchProcessorNode toResultsTree(ClassDescriptor descriptor, Prefet |
723 | 733 |
|
724 | 734 | // take a shortcut when no prefetches exist... |
725 | 735 | if (prefetchTree == null) { |
726 | | - return new ObjectResolver(context, descriptor, metadata.isRefreshingObjects()) |
| 736 | + // When results come from cache (not a refresh operation), don't refresh objects |
| 737 | + // to avoid clobbering newer in-memory state |
| 738 | + boolean refresh = metadata.isRefreshingObjects() && !shouldSkipRefresh(); |
| 739 | + return new ObjectResolver(context, descriptor, refresh) |
727 | 740 | .synchronizedRootResultNodeFromDataRows(normalizedRows); |
728 | 741 | } else { |
729 | | - HierarchicalObjectResolver resolver = new HierarchicalObjectResolver(context, metadata); |
| 742 | + // When results come from cache (not a refresh operation), wrap metadata to prevent refreshing objects |
| 743 | + QueryMetadata effectiveMetadata = shouldSkipRefresh() && metadata.isRefreshingObjects() |
| 744 | + ? new NonRefreshingQueryMetadataWrapper(metadata) |
| 745 | + : metadata; |
| 746 | + HierarchicalObjectResolver resolver = new HierarchicalObjectResolver(context, effectiveMetadata); |
730 | 747 | return resolver |
731 | 748 | .synchronizedRootResultNodeFromDataRows(prefetchTree, normalizedRows, prefetchResultsByPath); |
732 | 749 | } |
733 | 750 | } |
| 751 | + |
| 752 | + private boolean shouldSkipRefresh() { |
| 753 | + // Skip refresh only for cache hits to prevent stale cached data from clobbering newer in-memory state |
| 754 | + // For cache misses (including explicit refresh operations), cacheHit is false, so refresh happens normally |
| 755 | + // Prefetch relationships are resolved independently via connectToParents(), so this doesn't affect prefetch behavior |
| 756 | + return cacheHit; |
| 757 | + } |
734 | 758 |
|
735 | 759 | protected void performPostLoadCallbacks(PrefetchProcessorNode node, LifecycleCallbackRegistry callbackRegistry) { |
736 | 760 |
|
@@ -1019,4 +1043,18 @@ Object convert(Object object) { |
1019 | 1043 | } |
1020 | 1044 | } |
1021 | 1045 |
|
| 1046 | + /** |
| 1047 | + * Wrapper that overrides isRefreshingObjects() to return false, preventing cached |
| 1048 | + * query results from clobbering newer in-memory object state. |
| 1049 | + */ |
| 1050 | + static class NonRefreshingQueryMetadataWrapper extends QueryMetadataProxy { |
| 1051 | + NonRefreshingQueryMetadataWrapper(QueryMetadata delegate) { |
| 1052 | + super(delegate); |
| 1053 | + } |
| 1054 | + |
| 1055 | + @Override |
| 1056 | + public boolean isRefreshingObjects() { |
| 1057 | + return false; |
| 1058 | + } |
| 1059 | + } |
1022 | 1060 | } |
0 commit comments