Is it inefficient to use count() in Laravel Blade? - laravel

This is more a "best practice" question.
My website has articles, and articles can have comments.
public function comments() {
return $this->hasMany('App\Comment', 'submission_id');
}
On the front page, where the articles are listed, the amount of comments are shown:
#if ($article->comments->count())
{{ $article->comments->count() }}
{{ $article->comments->count() == 1 ? 'comment' : 'comments' }}
#endif
And I was thinking to myself, in this example, is my database being queried 3 times for this simple code snippet?
Every time I get the comment count through this relationship, is it loading the entire array of comments and all of its columns? Because sometimes articles can have upwards of 1k+ comments.
If so, would it not be best practice to simply have a column in my articles database table that increments every time a comment is posted and then fetching that directly rather than through the relationship?

Not the way you're doing it. It depens on when you're using ->count(), and whether or not it's the Collection version, or the Builder version. See the following examples:
Example one - new query with comments() method:
$article = Article::first();
$article->comments()->count();
In this example, using $article->comments() causes a new query to be started (using the Builder class). No matter how you finish this query, i.e. using ->first(), ->get() or ->count() a new query will be executed.
Now, example two - new query with comments property:
$article = Article::first();
$article->comments->count();
In this example, $article->comments will be executing a new query, as relationship accessors act differently if the relationship has been loaded or not. In this case, it hasn't been loaded, so $article->comments returns a Builder instance instead of a Collection, so finishing the query will execute a new query.
Finally, example three - Collection access with comments property:
$article = Article::with(["comments"])->first();
$article->comments->count();
In this last example, $article->comments has been specified as loaded via the ->with(["comments"]) clause, so it is a Collection, and not a Builder instance. For this reason, calling ->count() is using the Collection count method, and does not perform an additional query

Related

What is the different between laravel WITH and LOAD [duplicate]

I really tried to understand the difference between the with() method and the load() method, but couldn't really understand.
As I see it, using the with() method is "better" since I eager load the relation. It seems that if I use load() I load the relation just as if I would use the hasMany() (or any other method that relates to the relation between objects).
Do I get it wrong?
Both accomplish the same end results—eager loading a related model onto the first. In fact, they both run exactly the same two queries. The key difference is that with() eager loads the related model up front, immediately after the initial query (all(), first(), or find(x), for example); when using load(), you run the initial query first, and then eager load the relation at some later point.
"Eager" here means that we're associating all the related models for a particular result set using just one query, as opposed to having to run n queries, where n is the number of items in the initial set.
Eager loading using with()
If we eager load using with(), for example:
$users = User::with('comments')->get();
...if we have 5 users, the following two queries get run immediately:
select * from `users`
select * from `comments` where `comments`.`user_id` in (1, 2, 3, 4, 5)
...and we end up with a collection of models that have the comments attached to the user model, so we can do something like $users->comments->first()->body.
"Lazy" eager loading using load()
Or, we can separate the two queries, first by getting the initial result:
$users = User::all();
which runs:
select * from `users`
And later, if we decide that we need the related comments for all these users, we can eager load them after the fact:
$users = $users->load('comments');
which runs the 2nd query:
select * from `comments` where `comments`.`user_id` in (1, 2, 3, 4, 5)
...and we end up with the same result, just split into two steps. Again, we can call $users->comments->first()->body to get to the related model for any item.
Why use load() vs. with()? load() gives you the option of deciding later, based on some dynamic condition, whether or not you need to run the 2nd query. If, however, there's no question that you'll need to access all the related items, use with().
The alternative to either of these would be looping through the initial result set and querying a hasMany() relation for each item. This would end up running n+1 queries, or 6 in this example. Eager loading, regardless of whether it's done up-front with with() or later with load(), only runs 2 queries.
As #damiani said, Both accomplish the same end results—eager loading a related model onto the first. In fact, they both run exactly the same two queries. The key difference is that with() eager loads the related model up front, immediately after the initial query (all(), first(), or find(x), for example); when using load(), you run the initial query first, and then eager load the relation at some later point.
There is one more difference between With() & load(), you can put the conditions when using with() but you can't do the same in case of load()
For example:
ProductCategory::with('children')
->with(['products' => function ($q) use($SpecificID) {
$q->whereHas('types', function($q) use($SpecificID) {
$q->where('types.id', $SpecificID)
});
}])
->get();
#damiani Explanied difference between load() and with() as well but he said load() is not cacheable so I wanna say couple words about it.
Let assume we have a blog post and related with comments. And we're fetching together and caching it.
$post = Cache::remember("post.".$slug,720,function()use($slug){
return Post::whereSlug($slug)->with("comments")->first();
});
But if there is a new comment and we want to display it immediately, we have to clear post cache and fetch post and comments together again. And that causes unnecessary queries. Lets think there are another queries for tags, media, contributors of the post etc. it will increase amount of resource usage..
public function load($relations)
{
$query = $this->newQueryWithoutRelationships()->with(
is_string($relations) ? func_get_args() : $relations
);
$query->eagerLoadRelations([$this]);
return $this;
}
As you can see above when we use the method it loads given relation and returns model with fetched relation. So you can return it outside of a callback.
$post = Cache::remember("post.".$slug,720,function()use($slug){
return Post::whereSlug($slug)->first();
});
$post = Cache::remember("post.relation.images.".$slug,720,function()use($post){
return $post->load("images");
});
$post = Cache::remember("post.relation.comments".$slug,720,function()use($post){
return $post->load("comments");
});
So if we load them seperatly, next time when some of them updated all you need to do clear specific relation cache and fetch it again. No need to fetch post, tags, images etc. over and over.

Eloquent resulting single search result as array not collection

I have some instances where a eloquent is resulting an single array not a collection. Although dd shows it as a collection with a single entry.
For example I have a query in a controller:
$pg = Page::with('getPanels')->where('slug',$slug)->get();
This will return a single result and works fine, so I pass this to a blade template. My complete function is
$pg = Page::with('getPanels')->where('slug',$slug)->get();
return view('front.page',['pg' => $pg]);
As soon as he template is brought in it will fall over at
if (!is_null($pg->headImage))
{$img = asset('images/pages')."/".$pg->headImage;}
and I will get
Property [headImage] does not exist on this collection instance.
If I change the line to
if (!is_null($pg[0]['headImage']))
it will continue OK. This is of course a pain as I would much rather use $pg->headImage.
Can someone enlighten me please?
I have sorted this and I hope it will help other people.
If I use
$pg = Page::with('getPanels')->where('slug',$slug)->first();
it will be just one result (naturally) and therefore
$pg->headImage
will fail as it wants
$pg[0]['headImage']
but if I change the eloquent instead of get(0 to first() (still just one result)
$pg = Page::with('getPanels')->where('slug',$slug)->first();
I can use $pg->headImage or what field I want.

Laravel queryScope

I created a queryScope
public function scopeCtmpActive($query)
{
return $query->where('ctmp_active', 'y');
}
Then I replace following line
$customtemplates_collection = Auth::user()->customtemplates->where('ctmp_active', 'y')->sortByDesc('ctmp_id');
with
$customtemplates_collection = Auth::user()->customtemplates->ctmpActive()->sortByDesc('ctmp_id');
And I am getting following FatalErrorException
Call to undefined method Illuminate\Database\Eloquent\Collection::ctmpActive()
How am I suppose to use a query exception with a relationship?
As the name implies, "query" scopes are for reusable, common "query" constraints.
$customtemplates_collection = Auth::user()->customtemplates;
This returns a collection. You are getting all "customtemplates" that belong to the authenticated user. Then, Laravel is nice in that the Collection class allows for a nice way to filter out results, which is why the next part works:
$customtemplates_collection = Auth::user()->customtemplates->where('ctmp_active', 'y');
You are using PHP. Not MySQL. To emphasize, you are getting every single "customtemplates" that belongs to the user, and then using (PHP) Laravel Collection's where method to go through each one and filter out the results. You are not adding a where clause to the query. That's why the above works.
However, query scopes are for query constraints so they need to happen during the query, not after. What you probably want is something like this:
$customtemplates_collection = Auth::user()->customtemplates()->ctmpActive()->orderBy('ctmp_id', 'desc')->get();
When you add the paranthesis after customtemplates(), you are invoking the customtemplates method. In this case, I'm assuming it's a HasMany relationship so it'll return a HasMany instance. Then basically, it uses PHP's magic method (__call) to build the query builder so each method after that is essentially prepping the database query. Then, when you're finished building the query, you call get to fetch the results.

laravel with() method versus load() method

I really tried to understand the difference between the with() method and the load() method, but couldn't really understand.
As I see it, using the with() method is "better" since I eager load the relation. It seems that if I use load() I load the relation just as if I would use the hasMany() (or any other method that relates to the relation between objects).
Do I get it wrong?
Both accomplish the same end results—eager loading a related model onto the first. In fact, they both run exactly the same two queries. The key difference is that with() eager loads the related model up front, immediately after the initial query (all(), first(), or find(x), for example); when using load(), you run the initial query first, and then eager load the relation at some later point.
"Eager" here means that we're associating all the related models for a particular result set using just one query, as opposed to having to run n queries, where n is the number of items in the initial set.
Eager loading using with()
If we eager load using with(), for example:
$users = User::with('comments')->get();
...if we have 5 users, the following two queries get run immediately:
select * from `users`
select * from `comments` where `comments`.`user_id` in (1, 2, 3, 4, 5)
...and we end up with a collection of models that have the comments attached to the user model, so we can do something like $users->comments->first()->body.
"Lazy" eager loading using load()
Or, we can separate the two queries, first by getting the initial result:
$users = User::all();
which runs:
select * from `users`
And later, if we decide that we need the related comments for all these users, we can eager load them after the fact:
$users = $users->load('comments');
which runs the 2nd query:
select * from `comments` where `comments`.`user_id` in (1, 2, 3, 4, 5)
...and we end up with the same result, just split into two steps. Again, we can call $users->comments->first()->body to get to the related model for any item.
Why use load() vs. with()? load() gives you the option of deciding later, based on some dynamic condition, whether or not you need to run the 2nd query. If, however, there's no question that you'll need to access all the related items, use with().
The alternative to either of these would be looping through the initial result set and querying a hasMany() relation for each item. This would end up running n+1 queries, or 6 in this example. Eager loading, regardless of whether it's done up-front with with() or later with load(), only runs 2 queries.
As #damiani said, Both accomplish the same end results—eager loading a related model onto the first. In fact, they both run exactly the same two queries. The key difference is that with() eager loads the related model up front, immediately after the initial query (all(), first(), or find(x), for example); when using load(), you run the initial query first, and then eager load the relation at some later point.
There is one more difference between With() & load(), you can put the conditions when using with() but you can't do the same in case of load()
For example:
ProductCategory::with('children')
->with(['products' => function ($q) use($SpecificID) {
$q->whereHas('types', function($q) use($SpecificID) {
$q->where('types.id', $SpecificID)
});
}])
->get();
#damiani Explanied difference between load() and with() as well but he said load() is not cacheable so I wanna say couple words about it.
Let assume we have a blog post and related with comments. And we're fetching together and caching it.
$post = Cache::remember("post.".$slug,720,function()use($slug){
return Post::whereSlug($slug)->with("comments")->first();
});
But if there is a new comment and we want to display it immediately, we have to clear post cache and fetch post and comments together again. And that causes unnecessary queries. Lets think there are another queries for tags, media, contributors of the post etc. it will increase amount of resource usage..
public function load($relations)
{
$query = $this->newQueryWithoutRelationships()->with(
is_string($relations) ? func_get_args() : $relations
);
$query->eagerLoadRelations([$this]);
return $this;
}
As you can see above when we use the method it loads given relation and returns model with fetched relation. So you can return it outside of a callback.
$post = Cache::remember("post.".$slug,720,function()use($slug){
return Post::whereSlug($slug)->first();
});
$post = Cache::remember("post.relation.images.".$slug,720,function()use($post){
return $post->load("images");
});
$post = Cache::remember("post.relation.comments".$slug,720,function()use($post){
return $post->load("comments");
});
So if we load them seperatly, next time when some of them updated all you need to do clear specific relation cache and fetch it again. No need to fetch post, tags, images etc. over and over.

Eager load single item with Eloquent belongsToMany

I have a Batch model, which hasMany Results and belongsTo a Project. The current status of a batch is based on the status of its most recent Result. So, in my batch model I have this:
public function allForProject($pid)
{
$batches = $this
->with(static::$relatedObjects)
->with('current_status')
->where('project_id', '=', $pid)
->get();
return $batches;
}
public function current_status()
{
return $this
->belongsToMany('BehatEditor\Models\Result')
->orderBy('created_at', 'DESC')
->limit(1)
;
}
...So by saying "->with('current_status')" I am trying to eager load only the most recent result for that batch - there may be thousands of them per batch that I do not want to return to the front end.
Now, this doesn't break, but the "limit(1)" actually seems to limit the number of Batches that get returned with a Result. Even though each one of my Batches has 2 results with my test data, when I use limit(1) only one of them comes back with any data. When I use limit(2), only 2 batches come back with a single current_status record (desired) but the rest have an empty array for current_status.
This is a Silex project using Eloquent as an ORM, so Laravel specific methods won't work.
Any help is much appreciated!
UPDATE:
It looks like Eloquent just doesn't support this. see http://irclogs.julien-c.fr/2013-12-19/01:48#log-52b25061a599aafb54008650. I would like to update my question to be how can I cleanly add the raw SQL I need to my query? Can I supply my own method that holds only the SQL needed, or do I need to replace all ORM usage in allForProject()?
Instead of ->limit(1), use ->first().
Update: Misread what you said, this should work.

Resources