ofMany eager-loaded relations return null on later paginated pages after cache warmup
Describe the bug
When a paginated Eloquent query eager-loads a cached HasOne::ofMany() relation, the relation is correct on the first page but may be null on subsequent pages after the related-model cache has been warmed.
Laravel builds the aggregate query used by ofMany() lazily through a beforeQuery callback. During eager loading, Laravel adds the current page's parent IDs to the joined subquery immediately before executing it.
Laravel Model Caching generated the related-model cache key before that callback ran. Consequently, the key did not include the joined subquery or its page-specific bindings. Different pages could therefore receive the same related-model cache key.
Page two would then retrieve page one's related models from the cache. Eloquent could not match those models to the parents on page two, so it initialized the eager-loaded relation as null.
This also affects the convenience methods oldestOfMany() and latestOfMany() because they use the same underlying query behavior.
Eloquent Query
Example relationship:
use Illuminate\Database\Eloquent\Relations\HasOne;
public function oldestBook(): HasOne
{
return $this->hasOne(Book::class)
->ofMany('id', 'min');
}
Reproduction:
$pageOne = Author::query()
->with('oldestBook')
->orderBy('name')
->paginate(
perPage: 1,
page: 1,
);
$pageTwo = Author::query()
->with('oldestBook')
->orderBy('name')
->paginate(
perPage: 1,
page: 2,
);
// Correct:
$pageOne->first()->oldestBook;
// Incorrect after the relation cache was populated by page one:
$pageTwo->first()->oldestBook; // null
Stack Trace
There is no stack trace. The query completes successfully but returns incorrect cached relationship data. The visible symptom is a silently missing (null) eager-loaded relation on later pages.
Environment
Environment in which the behavior was reproduced:
- PHP: 8.5.8
- OS: macOS 26.5.2
- Laravel: 13.19.0
- Model Caching: 13.1.6
- Cache store: Redis
- Database: SQLite
The issue is caused by Laravel's deferred ofMany() query construction rather than by a particular database engine.
Additional context
Initial investigation indicates that the related query's cache key is created before Laravel calls its registered beforeQuery callbacks. At that point, neither the ofMany() joined-subquery SQL nor its parent-ID bindings are present in the query used to generate the key.
Additionally, the existing bindings slug appears to be derived from a fresh model query rather than from the related query being cached. It therefore cannot distinguish the eager-load queries for different pages.
A possible solution would be to incorporate the materialized deferred query into cache-key generation. This would likely require:
- Detect related queries with deferred
beforeQuery callbacks.
- Clone the underlying query builder so cache-key generation does not mutate the query that Laravel will execute.
- Apply the clone's callbacks to materialize the deferred joins.
- Hash the materialized SQL and bindings.
- Append a suffix to the existing cache key.
Queries without deferred callbacks should retain their previous cache-key format to avoid unnecessarily invalidating ordinary cache entries.
ofManyeager-loaded relations return null on later paginated pages after cache warmupDescribe the bug
When a paginated Eloquent query eager-loads a cached
HasOne::ofMany()relation, the relation is correct on the first page but may benullon subsequent pages after the related-model cache has been warmed.Laravel builds the aggregate query used by
ofMany()lazily through abeforeQuerycallback. During eager loading, Laravel adds the current page's parent IDs to the joined subquery immediately before executing it.Laravel Model Caching generated the related-model cache key before that callback ran. Consequently, the key did not include the joined subquery or its page-specific bindings. Different pages could therefore receive the same related-model cache key.
Page two would then retrieve page one's related models from the cache. Eloquent could not match those models to the parents on page two, so it initialized the eager-loaded relation as
null.This also affects the convenience methods
oldestOfMany()andlatestOfMany()because they use the same underlying query behavior.Eloquent Query
Example relationship:
Reproduction:
Stack Trace
There is no stack trace. The query completes successfully but returns incorrect cached relationship data. The visible symptom is a silently missing (
null) eager-loaded relation on later pages.Environment
Environment in which the behavior was reproduced:
The issue is caused by Laravel's deferred
ofMany()query construction rather than by a particular database engine.Additional context
Initial investigation indicates that the related query's cache key is created before Laravel calls its registered
beforeQuerycallbacks. At that point, neither theofMany()joined-subquery SQL nor its parent-ID bindings are present in the query used to generate the key.Additionally, the existing bindings slug appears to be derived from a fresh model query rather than from the related query being cached. It therefore cannot distinguish the eager-load queries for different pages.
A possible solution would be to incorporate the materialized deferred query into cache-key generation. This would likely require:
beforeQuerycallbacks.Queries without deferred callbacks should retain their previous cache-key format to avoid unnecessarily invalidating ordinary cache entries.