← Back to blog

Eloquent Performance: Fake relationships

| Laravel

This article was published over 2 years ago. Some information may be outdated.

In the previous post I discussed subqueries, an efficient way to fetch a subset of data from another table.

In this post, I will extend upon the subquery concept and introduce fake relationships.

Fake relationship is the name I use for this particular technique. Other people may use different names.

In the previous post, we saw how to fetch the latest_login_date from the App\Models\Login efficiently.

What if you want to get the latest App\Models\Login as an Eloquent model instance and not just a Carbon instance?

You could add a method to fetch the last login in the App\Models\User:

// App\Models\User

public function latestLogin()
{
    return App\Models\Login::where('user_id', $this->id)->latest()->first();
}

But this introduces an N+1 problem, since the App\Models\Login will be loaded for each user individually.

Here is how to solve it using a fake relationship.

Fake relationships

You can create a fake relationship and make Laravel treat it as if it were real.

Add a new fake relationship in the App\Models\User:

public function lastLogin()
{
    return $this->belongsTo(App\Models\Login::class);
}

This is a fake relationship because there is no last_login_id in the App\Models\User, which is required for the belongsTo relationship to work normally.

The solution is to combine the subquery with eager-loading to make the lastLogin relationship work:

public function scopeWithLastLogin($query)
{
    $query->addSelect(['last_login_id' => Login::select('id')
        ->whereColumn('user_id', 'users.id')
        ->latest()
        ->take(1)
    ])->with('lastLogin');;
}
// App\Http\Controllers\UsersController

// ...

User::withLastLogin()->paginate();

// ...

If you look at the debugger you will see that all the latest logins were eager-loaded: Eager-loading with fake realtionships

The lastLogin method cannot be used without calling the withLastLogin scope on the App\Models\User.

In the next post, I will discuss reusable relationships.

Summary

  • Fake relationships -- define a belongsTo relationship to a model that does not have an actual foreign key column. The subquery provides the key value at runtime.
  • Combine subqueries with eager-loading -- use addSelect to inject the foreign key via a subquery, then use with() to eager-load the relationship. This avoids N+1 queries entirely.
  • Scope dependency -- the fake relationship only works when the corresponding scope is called, because the scope provides the subquery that populates the foreign key.
Share