Skip to content

Cached ofMany eager-loaded relations return null on later paginated pages #604

Description

@ezp-matt

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:

  1. Detect related queries with deferred beforeQuery callbacks.
  2. Clone the underlying query builder so cache-key generation does not mutate the query that Laravel will execute.
  3. Apply the clone's callbacks to materialize the deferred joins.
  4. Hash the materialized SQL and bindings.
  5. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions