Querying collections in Laravel - php

Have I got it correctly that when I query a Laravel collection, it doesn't query the database but executes the query on what was already fetched?
For example, I have a relation that returns a collection:
public function permissions()
{
return $this->belongsToMany(Permission::class, RolePermission::getModelTable(), 'role_id', 'permission_id');
}
Does the following code query the database or it works with the collection using php facilities?
$role->permissions->where('code','global.test')->count()
And, as far as I understand, if I query the relationship then the database will be queried instead of working with the results that were already fetched:
$role->permissions()->where('code','global.test')->count()
So basically, $role->permissions - working with the fetched results "offline", but $role->permissions() - querying the database
What way is generally more efficient and when?

You're basically right. The difference between calling $role->permissions and $role->permissions() is that the first returns an instance of Collection, while the second returns an instance of BelongsToMany.
Collection is a collection (really?) of related objects and BelongsToMany is the relation itself. So yes, by calling the method (and not the magic property) you are querying the database.
Update
I didn't get the last question, sorry.
The first time you call $role->permissions (magic property), Laravel fetches all of the permissions associated with $role, if they were not eager loaded. If you need only a subset of those permissions, you can filter them by using any of the magic property and the method. Let me do some examples.
$role = Role::first();
// Fetch all the permissions and count a subset.
$role->permissions->where('code', 'global.test')->count();
// Count another subset.
$role->permissions->where('code', 'another.test')->count();
The same can be done using the method:
$role = Role::first();
// Fetch a subset of the permissions and count it.
$role->permissions()->where('code', 'global.test')->count();
// Fetch another subset and count it.
$role->permissions()->where('code', 'another.test')->count();
As you can see, in the first example you make only one query and filter the results differently. In the second example you make two queries. The first one is obviously more efficient.
If you need only a subset during the same execution, though, things change. Here we are using eager loading:
$role = Role::with('permissions', function($query) {
// Here we filter the related permissions.
$query->where('code', 'global.test');
})->first();
// We have what we want. No need to filter the collection.
$role->permissions->count();
// Let's do something else with this subset.
$role->permissions->all();
What if you fetch all the related objects, but need only that subset?
$role = Role::first();
// Filter the collection to count the needed subset.
$role->permissions->where('code', 'global.test')->count();
// Filter the collection to get the needed subset.
$role->permissions->where('code', 'global.test')->all();
As you can see, in the second example we are much less DRY, and we are also doing the same operation multiple times. Less efficient, of course.

Related

Laravel - Differences between two solutions of a method in a controller

A stack overflow user answered me two ways to return each person's furniture in a 'one to many' relationship. This worked well.
My question is to know the difference in the two ways. The advantages and disadvantages of each way.
IMPORTANT: Laravel Version: 5.8
First solution:
public function showPersonFurnitures($id) {
$person = Person::with('furnitures')->findOrFail($id);
$furnituresOfEachPerson = $person->furnitures; //<-----
return response()->json($furnituresOfEachPerson);
}
Second solution:
public function showPersonFurnitures($id) {
$person = Person::with('furnitures')->findOrFail($id);
$furnituresOfEachPerson = $person->furnitures()->get(); //<----
return response()->json($furnituresOfEachPerson);
}
The given answers are vague and incomplete, so here's a better explanation hopefully:
Situation 1 $person->furnitures(): When you call any of a model's relations as a function, you get an (incomplete) query object for that particular relation. That means whenever you call $person->furnitures(), any additional functions you chain on this result like ->where() are actual SQL operators and directly modify the query. In order to complete the query, you should call ->get() (or a function like ->pluck()) at the end to retrieve the actual data.
Situation 2 $person->furnitures: When you call any of a model's relations as a property, you retrieve the complete relation collection for that model. This means that $person->furnitures lazy-loads the collection if it is not available on the model yet. Any additional functions you chain on this result like ->where() will act on the PHP collection.
By using Person::with('furnitures')... you make sure that the relation is already loaded (Eager Loaded) after the findOrFail() call. You might note that this is not particularly useful with respect to loading 1 model (since you might just as well call $person->furnitures whenever you need it), but the important thing to remember here is that this with() method is extremely useful when you are retrieving collections of a model, like $persons = Person::where('activated', 1)->with('furnitures')->get(). This last query only executes 2 queries: 1 for retrieving persons, and 1 for retrieving all furniture related to these persons.
Overall, the difference for those 2 methods are not huge and you will get the same result, except:
$person->furnitures uses relationships, quite similar to eager loading that it retrieves data based on the One to Many relationship.
$person->furnitures()->get() is implemented from the perspective of collection, which is why it has the syntax of querying a collection.
They are same, but $person->furnitures requires extra steps to understand tha this is relation, and finally call $person->furnitures()->get()

question about laravel relationships and performance

i hope you are having a good time. i am learning laravel and the inscuctor talked about when you load relationships in laravel, like so
public function timeline()
{
$ids = $this->follows()->pluck('id');
$ids->push($this->id);
return Tweet::whereIn('user_id', $ids)->latest()->get();
}
and i have a follows relationship in my model, and he talked about this line
$ids = $this->follows()->pluck('id');
being better for performance than this line
$ids = $this->follows->pluck('id');
my question is, how does laravel pluck the ids in the first case, and how it queries the database
i hope im making sense, thanks for your time, and answer.
the following one executes a select query on database
$this->follows()->pluck('id');
the follows() returns a query builder (which is a not yet executed sql statement) and then on the result select the id column and returns a collection of ids
you can see the query by dumping the query builder by $this->follows()->dd()
Whereas in the second option
$this->follows->pluck('id')
up until $this->follows laravel executes a query and returns all the records as a collection instance, You will be able to see all the attributes on each of the records. And then ->pluck('id') is getting executed on the laravel collection class, which will do an operation I assume similar to the array_column function does and returns only the id column.
as you can easily see in the second operation the whole data set was retrieved first from the DB and then selected the required attribute/column (2 distinct and heavy operations). Where as in the first option we directly told eloquent to select only the required column, which is only one lighter operation compared to the second option.

How to sort a Cakephp query using a post calculated field and pass it to the paginate?

Have a formatResults callback function that adds a "custom calculated" field into the entities post returned from a model query in my Cakephp. I would like to sort by this field and use this on a paginate is this possible?
So far i cannot accomplish this because the paginate limits the records fetched and therefore only records less than the paginator limit get sorted and not all the resultset...
Current code:
$owners = $this->Owners->find('all');
$owners->formatResults(function (\Cake\Collection\CollectionInterface $owners) {
$owners = $owners->map(function ($entity) {
$entity->random = rand(0,1);
return $entity;
});
return $owners->sortBy(function ($item){
return $item->random;
},SORT_DESC);
});
This works as expected:
$owners->toArray();
This does not:
$owners = $this->paginate($owners);
$owners->toArray();
Mainly because its "callback processing" only the first 10 records, i would like to process the whole resultset.
After diggin around ive found a similar topic opened by a previous user on the this link, it seems that is not possible to use pagination sort in other than the fields in the database.
As a result, i would suggest:
1 - Either alter your model logic, to accommodate your requirements by creating virtual fields or alter database schema to include this data.
2 - If the data requires further or live processing and it cannot be added or calculated in the database, perhaps programming a component that will replicate the paginate functionality on a cakephp collection would be a good option.The downside of this approach is that all records will be returned from the database which may present performance issues on large resultsets.

Laravel, too many queries, if I manipulate the returned Eloquent collection

I'm using Laravel 5.7 and some of basic relations between 2 tables
when I try to do anything with the returned Eloquent collection,
I find the DebugBar records a lot of queries
like this example:
$articles = \App\Article::get();
return count($articles->toArray());
// DebugBar records about 395 queries
On the contrary, If use the collection counting helper
$articles = \App\Article::get();
return $articles->count();
// DebugBar records only 2 queries
Collections are kind of lazy evaluation when it comes to fetching relationships. When you convert that to an array more queries need to be run to fetch more data. If it's a collection, its a revokable object and can chain methods further to get relationships and will act only according to the successive call.

How do I efficiently limit query to the presence of an id in a many-to-many relationship in laravel?

The scenario:
I am building a search system for a given set of data. Most of this is straight forward - where record title contains X, where record date before Y, etc. Where I am running into difficulty is essentially a category search. Each record belongs to zero or more categories (a relationship pivot table exists such that each row contains a record and a category), and when the user searches for Category A, I want to return all of the records that belong to that category. I've gotten this working with a whereHas, but it seems inordinately slow. In this instance, assume $category is a numeric id that is correctly validated as a category, and $records is an eloquent query builder that has not yet been executed by a get, pagination, all, etc (my function checks to see if several $request->input parameters are defined, then attaches a where to $record as required by the specified parameters, only executing it after all parameters have been considered):
if(!empty($category)) {
$records = $records->whereHas('categories', function($query) use ($category)
{
$query->where('category_id', $category);
});
}
This works, but as there are 7000+ records, 7000+ relationships defined in the pivot, and roughly 30 'categories', the search takes longer than I am comfortable leaving it. My unconfirmed thought is that the where query is executing for every record, thus leading to hundreds or thousands of queries.
I've debated approaching this using the raw query builder and just passing the list of record id's that have that category and using a simple where to filter the record collection before it's executed, but it seems counter-intuitive, leading me to believe there must be a better way.
The Question
How do I efficiently limit the records returned by $records->get() to those records with a defined relationship to category $category.
Edit 2018-01-16
To clarify, while I could simply do $category->records to return all records belonging to a category, this is part of a larger search engine. The full structure of the code looks like this:
If($subject_search_term) {
$records->where('subject', $subject_search_term)
}
If(some other search criteria is defined) {
$records->where(someothercriteria);
}
If(Category search criteria is defined) {
$records->whereHas(something);
}
$records->paginate(20);
Furthermore, there are two of these many-to-many relationships that I need to query (in addition to 'categories', lets say there is also a 'subject' that is independent of it, but similar structure and idea). As far as I know, I need to build the query off records and filter it accordingly.
EDIT 2
For anyone else with this problem, it seems the vastly more efficient way (and the most efficient that I've found) is Joel Hinz's comment - use the DB facade to build a raw query, pluck the id's from it, and use that in a whereIn clause.
Try this:
mpyw/eloquent-has-by-non-dependent-subquery: Convert has() and whereHas() constraints to non-dependent subqueries.
mpyw/eloquent-has-by-join: Convert has() and whereHas() constraints to join() ones for single-result relations.
if(!empty($category)) {
$records = $records->hasByNonDependentSubquery('categories', function($query) use ($category)
{
$query->where('category_id', $category);
});
}
That's all. Happy Eloquent Life!

Categories