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:

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
belongsTorelationship 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
addSelectto inject the foreign key via a subquery, then usewith()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.