When working with relationships, most likely you need to get count of related models in many places. There are obvious ways to achieve this of course, but not always efficient. Especially when you are loading a collection of models and their relations.
So let me show you what we can do about it!
hasMany relation
1 2 3 4 5 6 7 |
// Post model public function comments() { return $this->hasMany('Comment'); } |
1. The most straighforward way would be using one of the two methods below (probably wrapped in a method like getCommentsCount
in Post
model):
1 2 3 4 5 6 7 8 9 10 11 12 |
[1] > $post = Post::first(); // object(Post)( // 'incrementing' => true, // 'timestamps' => true, // 'exists' => true // ) [2] > $post->comments->count(); // 4 [3] > $post->comments()->count(); // 4 |
However this is not the best already:
1. $post->comments->count();
loads the collection and returns count of its elements – redundant if all you need is the count.
2. $post->comments()->count();
better, no collection loading, but runs the query everytime we use it.
3. it DOESN’ SCALE 😉 When you need a collection of posts and comments count for each of them, then we end up with n+1 issue, because there’s no way to eager load the count. Of course we can use joins
and groupBy
and so on, but who cares! Why would you write something like this:
1 2 3 4 5 6 |
$posts = Post::leftJoin('comments', 'comments.post_id', '=', 'posts.id') ->select('posts.*', 'count(*) as commentsCount') ->groupBy('posts.id') ->get(); |
when we prefer nice and elegant syntax a’la Eloquent:
1 2 3 |
$posts = Post::with('commentsCount')->get(); |
Oh, crap… just spoiled the fun
Yes, this is exactly the piece we’re looking for, so let’s get it done already!
In order to let Eloquent eager load this thing, we must create a method returning Relation
object. And here it is:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// Post model public function comments() { return $this->hasMany('Comment'); } public function commentsCount() { return $this->comments() ->selectRaw('post_id, count(*) as aggregate') ->groupBy('post_id'); } |
That basically does the job, however I wouldn’t be myself if I left it just like that:
1 2 3 4 5 6 7 |
$post = Post::with('commentsCount')->first(); $post->commentsCount; // collection $post->commentsCount->first(); // Comment model with post_id and aggregate fields $post->commentsCount->first()->aggregate; // here's what we're looking for |
We saved n
queries, but we definitely didn’t save the world…
So let’s get it 2 steps forward:
1. Use hasOne
instead of hasMany
relation in order to avoid returning collection with single item
2. Use accessor in order to easily get the count
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// Post model public function commentsCount() { return $this->hasOne('Comment') ->selectRaw('post_id, count(*) as aggregate') ->groupBy('post_id'); } public function getCommentsCountAttribute() { // if relation is not loaded already, let's do it first if ( ! array_key_exists('commentsCount', $this->relations)) $this->load('commentsCount'); $related = $this->getRelation('commentsCount'); // then return the count directly return ($related) ? (int) $related->aggregate : 0; } |
As noted in the comments by Jeroen Noten Laravel/Illuminate 5.1 introduced relationLoaded
method, which does exactly the same as above array_key_exists
check, so you can use it instead for verbosity.
With this setup it’s soo nice and easy to work with:
1 2 3 4 5 6 7 8 9 |
// lazy loading $post = Post::first(); $post->commentsCount; // 4 // eager loading $posts = Post::with('commentsCount')->get(); $posts->first()->commentsCount; |
And that’s it for hasMany
!
Next we’re gonna work with belongsToMany