Eloquent Eager Loading Constrained by Pivot
The other day I was chatting with a coworker about using Laravel Eloquent ORM's wherePivot()
method to constrain eager loading using with()
while working with Many-to-Many relationships. Originally, the query that the coworker was trying was something like:
Article::where('id', 2)->with('authors')->wherePivot('display', true)->first();
However, that doesn't work because with()
returns an instance of \Illuminate\Database\Eloquent\Builder
whereas wherePivot()
needs to act on a \Illuminate\Database\Eloquent\Relations\BelongsToMany
instance.
During my time away from Laravel, I had forgotten how to do this offhand. While I found a StackOverflow post regarding the general way to constrain eager loads, I was unsure if much had changed in the last couple of years and if there were any improvements to the method that I had used previously.
Later I found the Laravel Docs entry on Eager Loading Constraints. My coworker and I agreed that the syntax is not as eloquent (pun intended) as one would hope when using Laravel. You can use a relational method to set up the relationship and constrain it, but that doesn't allow you to pass in dynamic values to constrain the relationship.
I put together an example showcasing the two different approaches with tests. The code is on Github.
There are three tables (all made up for this example): authors
, author_article
(the many-to-many pivot), and articles
. On the author_article
table, there is a boolean
property called display
which toggles whether or not a particular author should be displayed for an article
. Our goal is to be able to retrieve authors to display for articles or all authors for articles when we so desire.
The first way to constrain the eager loading based on the pivot is to chain a wherePivot()
onto a belongsToMany()
relationship.
public function authorsToDisplay()
{
return $this->belongsToMany(Author::class)->wherePivot('display', true);
}
When you find the particular article, you must call the relationship method defined in the with()
statement (authorsToDisplay
) instead of the normal authors
method. However, you almost always do not want to call the relation method when retrieving the relations given how Eloquent Eager loading works. When you call the relationship method instead of the parameter on the model corresponding to the model, it always fetches the data fresh (i.e. $article->authorsToDisplay()->get()
vs. $article->authorsToDisplay
).
$articlesWithAuthors = Article::where('id', 2)->with('authorsToDisplay')->first()->authorsToDisplay->toArray();
/*
Returns the following
[
[
"id" => 3,
"name" => "William Strunk",
"created_at" => null,
"updated_at" => null,
"pivot" => [
"article_id" => "2",
"author_id" => "3",
],
],
[
"id" => 5,
"name" => "E.B. White",
"created_at" => null,
"updated_at" => null,
"pivot" => [
"article_id" => "2",
"author_id" => "5",
],
],
]
*/
The other way to constrain the relation, using dynamic variables rather than statically coded ones in the relationship method, is to use the standard relationship method (authors()
in this case) and to constrain it when the with()
call is made. In the with()
call, you can use an array with array keys matching with the relationship method names correlated with array values of anonymous functions. The anonymous functions are passed a query object which can be used to constrain the eager load based on the pivot table or another constraint.
Here is what the authors' relationship looks like:
public function authors()
{
return $this->belongsToMany(Author::class);
}
Also, below is how the values could be retrieved. Additional constraints can be placed in the anonymous function to restrict the with()
eager loading further.
$articlesWithAuthors = Article::where('id', 2)
->with([
'authors' => function ($query) {
$query->wherePivot('display', true);
}
])->first();
/*
Returns the following
[
[
"id" => 3,
"name" => "William Strunk",
"created_at" => null,
"updated_at" => null,
"pivot" => [
"article_id" => "2",
"author_id" => "3",
],
],
[
"id" => 5,
"name" => "E.B. White",
"created_at" => null,
"updated_at" => null,
"pivot" => [
"article_id" => "2",
"author_id" => "5",
],
],
]
*/
Using Eager Loading Constraints in this fashion would be even more helpful in the case of a genuinely dynamic column where there could be N
number of values instead of a binary column. To see the Article class in action, you can view it in the repository.