Laravel Eloquent optimize request with many relationships - laravel

I have actually a DB structure with a dashboard_users table with several morphedByMany relationship with retailers, brands, private_groups tables, etc (there are several models which are morphed to this relation).
These relationships are handled through a roleables table.
So my dashboardUser model has these functions :
public function retailer()
{
return $this->morphedByMany(Retailer::class, 'roleable')
->withPivot('role_id')
->withTimestamps();
}
public function brand()
{
return $this->morphedByMany(Brand::class, 'roleable')
->withPivot('role_id')
->withTimestamps();
}
public function privateGroup()
{
return $this->morphedByMany(PrivateGroup::class, 'roleable')
->withPivot('role_id')
->withTimestamps();
}
Some dashboardUsers can have a relation with one or many Retailer, but also one or many Brand, or with some PrivateGroup. There is no rule here, a DashboardUser can have only one relation but also a hundred.
In my dashboard I have a screen where I need to manage all these relations and show which user has which relation.
Actually to retrieve all my DashboardUsers I'm making this simple Eloquent request :
$aUsersList = DashboardUser::with(['brand', 'retailerBrand', 'retailer', 'retailerGroup', 'privateGroup'])
->get()
->unique('uuid')
->all();
which is working fine... except that now that I have around 3k rows on each table the request takes more than 10 seconds to be executed.
I'm wondering how I could optimize this request to receive the same results.
I'm thinking of making several leftJoins this way but I'm stuck with 2 problems:
$aUsersList = DashboardUser::select("dashboard_users.*",
"retailers.uuid", "retailers.name", "retailers.postal", "roleables.role_id",
"brands.uuid", "brands.name",
"private_groups.uuid", "private_groups.name"
)
->leftJoin('roleables', function($query){
$query->on("dashboard_users.uuid", 'roleables.dashboard_user_uuid');
})
->leftJoin('retailers', function($query){
$query->on("retailers.uuid", 'roleables.roleable_id');
})
->leftJoin('brands', function($query){
$query->on("brands.uuid", 'roleables.roleable_id')
;
})
->leftJoin('private_groups', function($query){
$query->on("private_groups.uuid", 'roleables.roleable_id')
;
})
->get()
->all()
;
This request is much more quicker to be executed but it's not returning all the data (it seems to return only dashboardUsers with retailer relationship).
And also I fear I'll be loosing my relations this way (all the fields are aggregated in a single object). So is there a way to rehydrate my model with it's relations?
Last but not least I'm using Lumen V6 (so pretty close to Laravel 6).
Thank you for pushing me in the right direction.

Related

Get data on relation 'B' where relation 'A' does not exits in same id in laravel eloquent

I have two tables named "Student" and "Subscription". If there is at least one active plan in the subscription database, I check by date. I need to get the data if no plan is active. Below is the code I used to get the data. But this is wrong because in this query I am getting all the expired data and I should get the data only if there is not even one plan active in the student id.
$expired_student = Student::wherehas('getSubscription', function ($query) use ($current_date){
$query->where('expired_at','<',$current_date);
})->where('status',1)->count();
anyone can please help me to solve this problem
In you Student model, you can define a relationship method called activeSubscriptions, like this:
public function activeSubscriptions() {
return $this->hasMany(Subscription::class)->where(function($query) {
$query->whereNull('expired_at')->orWhere('expired_at', '>', now());
});
}
And you can use this function like this:
$expiredStudents = Student::doesntHave('activeSubscriptions')->get();

Prevent duplicate queries and N+1 problem in Laravel collection

I'm currently working on a simple Laravel project where I need to get the posts of the users I'm following. With the code below, I can get the posts but I also add a lot of duplicate queries and an N+1 issue on the Authenticated user. So it's becoming sort of a head scratcher. I've looked though other similar scenarios online but I haven't been able to pinpoint what I'm doing wrong. Perhaps there is a better way. Currently, I have on the User model:
public function usersImFollowing()
{
return $this->belongsToMany(User::class, 'follow_user', 'user_id', 'following_id')
->withPivot('is_following', 'is_blocked')
->wherePivot('is_following', true)
->wherePivot('is_blocked', false)
->paginate(3);
}
public function userPosts()
{
return $this->hasMany(Post::class, 'postable_id', 'id')
->where('postable_type', User::class);
}
As you can see, I am using two booleans to determine if a user is following or is blocked. Also, the Post model is a polymorphic model. There are several things I've tried, among them, I tried a hasManyThrough, without using the hasMany Posts relationship above. It got the posts for each user but since I'm using the booleans above, I couldn't use them in the hasManyThrough, it simply got the posts based on the following_id, whether or not the user was following or was blocked became irrelevant.
Then in a separate service class, I tried the methods below (I'm using a separate class to maintain the code easier). They both get the posts for each user but add an N+1 problem and 12 duplicate queries based on 5 posts from 2 users. I will also need to filter the results based on some conditions, so it will probably add more queries. Additionally, I'm using a Laravel resource collection that would pull other items for each post, such as images, comments, etc., so the amount of queries would increase even more. Not sure, perhaps I'm doing too much and there is an easier way:
Either:
$following = $request->user()->usersImFollowing();
$posts = $following->map(function($user){
return $user->userPosts()->get();
})->flatten(1);
return $posts;
Or
$postsfromfollowing = [];
$following = $request->user()->usersImFollowing()->each(function($user) use (&$postsfromfollowing){
array_push($postsfromfollowing,$user->userPosts);
});
$posts = Arr::flatten($postsfromfollowing);
return $posts;
Maybe you could use scopes to do little celanup of code and generated sql.
In User model something like
public function scopeIsFollowedBy(Builder $query, int $followerId) {
return $query->where('following_id', '=', $followerId);
}
And in Post model
public function scopeIsFollowedBy(Builder $query, int $followerId) {
return $query->whereHas('user', function($q) use ($followerId) {
$q->isFollowedBy($followerId);
});
}
You can use it then in coltroller like any other condition like this:
Post::isFollowedBy($followerId)->...otherConditions...->get();
The SQL generated won't go through foreach but only add one IF EXISTS select (generated by whereHas part of the code)
More on local scopes in Laravel is here https://laravel.com/docs/8.x/eloquent#local-scopes

Querying Distant Relationships

I have three tables: users, accounts and hotels. Users and Accounts are connected with belongstoMany relation and Accounts and Hotels are connected the same way. Each User has Accounts and those Accounts have Hotels.
When I have Auth::user(), how I can return all hotels?
$accounts = Auth::user()->accounts()->get();
With the above statement, I can get all Accounts. How can I return all Hotels?
WHAT I TRIED?
public function index(Request $request)
{
$accounts = Auth::user()->accounts()->get();
$hotels = collect();
foreach ($accounts as $key => $a) {
$h = $a->hotels();
$hotels = $hotels->toBase()->merge($h);
}
dd($hotels);
return view('hotels.index',compact('hotels'));
}
but this code dont return me hotels collection which I can use in blade view files
Case 1
In the case you have a relationship as shown in the diagram below
What you are looking for is the hasManyThrough relationship.
From the Laravel documentation
The "has-many-through" relationship provides a convenient shortcut for accessing distant relations via an intermediate relation
In your case, on your User model, you can define the relationship
public function hotels()
{
return $this->hasManyThrough('App\Hotel', 'App\Account');
}
To then get your collection of hotels you can simply use
$hotels = Auth::user()->hotels;
You can also provide extra arguments to the hasManyThrough function to define the keys that are used on each table, great examples of this are given in the documentation linked above!
Case 2
If, instead, you have a relation as shown in the following diagram
It is a little more tricky (or, at least, less clean). The best solution I can think of, that uses the fewest queries is to use with.
$accounts = Auth::user()->accounts()->with('hotels')->get();
Will give you a collection of accounts, each with a hotels child. Now, all we have to do is get the hotels as a standalone collection, this is simple with some neat collection functions provided by Laravel.
$hotels = $accounts->flatMap(function($account) {
return $account->hotels;
})->unique(function ($hotel) {
return $hotel->id;
});
This will do the job of creating a collection of hotels. In my opinion, it would be cleaner and more efficient to simply make a new relationship as shown below.
And then to perform queries, using basic Eloquent methods.

Laravel: How to retrieve this nested model

In Laravel I have ModelA, ModelB and ModelC. ModelA has many ModelB. ModelB has many ModelC. I want to retrieve all ModelC for a selection of ModelA. How do I do this?
I tried the following:
$models = ModelC::with(['modelB','modelB.modelA' => function ($query) {
$query->where('owner', 123);
}])->get();
But the first query in that case is select * from model_c. Obviously not the result I am looking for.
Imagine that you were received 100 objects from the database, and each record had 1 associated model (i.e. belongsTo). Using an ORM would produce 101 queries by default; one query for the original 100 records, and additional query for each record if you accessed the related data on the model object. In pseudo code, let’s say you wanted to list all published authors that have contributed a post. From a collection of posts (each post having one author) you could get a list of author names like so:
$posts = Post::published()->get(); // one query
$authors = array_map(function($post) {
// Produces a query on the author model
return $post->author->name;
}, $posts);
We are not telling the model that we need all the authors, so an individual query happens each time we get the author’s name from the individual Post model instances.
Eager Loading
As I mentioned, ORMs “lazy” load associations. If you intend to use the associated model data you can trim that 101 query total to 2 queries using eager loading. You just need to tell the model what you need it to load eagerly.
Here’s an example from the Rails Active Record guide on using eager loading. As you can see, the concept is quite similar to Laravel’s eager loading concept.
$posts = Post::with('author')->limit(100)->get();
I find that I receive better understanding by exploring ideas from a wider perspective. The Active Record documentation covers some examples that can further help the idea resonate.
I managed to solve this with nested whereHas calls as follows:
$models = modelC::whereHas('modelB', function ($query) {
$query->whereHas('modelA', function ($query) {
$query->where('owner', 123);
});
})->get();
Laravel to the rescue, yet again!

Filtering eager-loaded data in Laravel 4

I have the following setup:
Clubs offer Activities, which are of a particular Type, so 3 models with relationships:
Club:
function activities()
{
return $this->hasMany('Activity');
}
Activity:
function club()
{
return $this->belongsTo('Club');
}
function activityType()
{
return $this->hasMany('ActivityType');
}
ActivityType:
function activities()
{
return $this->belongsToMany('Activity');
}
So for example Club Foo might have a single Activity called 'Triathlon' and that Activity has ActivityTypes 'Swimming', 'Running', and 'Cycling'.
This is all fair enough but I need to show a list of ActivityTypes on the Club page - basically just a list. So I need to get the ActivityTypes of all the related Activities.
I can do that like so from a controller method that receives an instance of the Club model:
$data = $this->club->with(array('activities', 'activities.activityTypes'))->find($club->id)
That gets me an object with all the related Activities along with the ActivityTypes related to them. Also fair enough. But I need to apply some more filtering. An Activity might not be in the right status (it could be in the DB as a draft entry or expired), so I need to be able to only get the ActivityTypes of the Activities that are live and in the future.
At this point I'm lost... does anybody have any suggestions for handling this use case?
Thanks
To filter, you can use where() as in the fluent DB queries:
$data = Club::with(array('activities' => function($query)
{
$query->where('activity_start', '>', DB::raw('current_time'));
}))->activityType()->get();
The example which served as inspiration for this is in the laravel docs, check the end of this section: http://laravel.com/docs/eloquent#eager-loading
(the code's not tested, and I've taken some liberties with the property names! :) )
I think if you first constraint your relationship of activities, the activity types related to them will be automatically constrained as well.
So what I would do is
function activities()
{
return $this->belongsToMany('Activity')->where('status', '=', 'active');
}
and then your
$data = $this->club->with(array('activities', 'activities.activityTypes'))->find($club->id)`
query will be working as you would expect.

Resources