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()
Related
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.
I have a scenario like this:
I have User model that has an OneToMany relationships with the Post model.
I have a Hashtag model that has an OneToMany relationships with the Post model.
Recap: ONE user has MANY posts, ONE post belongs to ONE hashtag, ONE hashtag has MANY posts.
I would like to fetch only unique users records of an hashtag.
I'm able to do it in non scalable way (fetching all the posts first and then iterate filtering by user id), but I need to maintain scalability for large record numbers.
Edit: I saw a partial solution in Laravel docs.
Laravel eloquent has a method called unique().
With that method I can specify the parameter which should be unique in query.
In my case figured it out with:
$users = $hashtag->posts->unique('user_id');
But I can't paginate query in this way...
Has anyone a solution for that?
You have to paginate your main model caller.
I don't know exactly if this is can reproduce your actual scenario, but let's see this example:
Let's say you have the model hashtags
So let's say the code could be something like this:
$users = DB::table('hashtags AS tags')
->select('tags.*')
->join('[posts AS post','post.id','=','tags.id')
->distinct()
->paginate(5, ['tags.*']);
I don't know if your query it'll be very accurate, but for this, I believe the best approach it'll be you do a raw query, could look like it'll be costly to your database and the operation, but you can work around this approach indexing and partitioning your database.
Remembering always the eloquent sometimes even when we're building join queries could be more even costly to your database.
Since you're worried about scalability so the best thing could be for too could write a view to fetch all the data with proper indexing and partitioning.
Try to use DISTINCT
$users = $hashtag->posts()->selectRaw('DISTINCT(user_id) AS unique_user_id')->paginate(10);
OR
use groupBy()
$users = $hashtag->posts()->groupBy('name')->select('user_id')->paginate(10);
There's another very similar issue with an answer that was very helpful (and I'm currently using) but causes the n+1 query problem.
I'll outline my use case. Polymorphic many to many relationship
I have:
Location model
Vendor model
User Model
Contactable (custom pivot model - still fleshing this out)
Users can be marked as contacts (aka contactables) for both Locations and Vendors.
I need to not detach contactables when dissociating them (I need a record of the fact that a User was once a contact for a location or vendor) so I don't want to detach them, I need to mark them inactive.
I'll limit the scope of this scenario to the following fields in the contactables table:
active
user_ID
contactable_type
contactable_ID
So I'm executing:
$collectionOfLocationIds = $contactDetails->locations()->getRelatedIds(); //changed to 'allRelatedIds()' in 5.4+
foreach ($collectionOfLocationIds as $locationID)
{
$contactDetails->locations()->updateExistingPivot($locationID, ['active' => 0]);
}
This runs great for most of my vendors, but some have 5k+ locations, so then I'm executing 5k+ update operations for what should really be one query. DB lives on a different server, so a few extra milliseconds add up pretty quickly...
I tried passing an array of ids to the updateExistingPivot function (it says it will take a mixed type for the id parameter) it doesn't produce an error, but it only seems to update the first id in the array. I'm not sure if this is a new bug, #Wallace Maxters mentioned that he could pass an array in 4.2, and I am still working in 5.3, but I'm wondering if anyone else has had this problem.
(Updated for clarity)
Use raw query instead of relationship.
I don't exactly understand which rows you want to inactive.
So lets think you want to inactive contactable for a particualar user.
If its not, change it to whatever where().
DB::table('contactable')->where('user_id', $user_id)
->update(['active' => 0]);
this will only execute one query.
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.
The problem
Is there a way to get an eloquent model and all its relationships using only one query?
Example scenario:
You have both Post and Comment Eloquent models.
You add their relationships in the model class using hasMany('Comment') and belongsTo('Post') respectively.
Now this is what i've been doing to retrieve both the post AND its comments:
$post = Post::find($id);
$post->comments;
return $post;
This returns a beautiful jsonobject. The problem is, this way, i would be using two queries. That's not so great.
Workaround
Two alternatives come to mind:
Using joinstatements in order to make the query i want. But Eloquent is so much more elegant.
Leveraging the Cache class in order to make even fewer queries in the future (this, i will do anyway later on).
Any ideas?
In eloquent you can do it like this
$post = Post::with('comments')->find($id);
return $post;
Edit:
This will not run the query with join in mysql but its just a single query in Eloquent.