Lazy Eager Loading for third level relationship in Laravel - laravel

UserModel has many lead and each lead can have one propertyLead and each propertyLead can have many attachments.
Each model is listed below,
UserModel:
public function leads()
{
return $this->hasMany('App\Models\Leads', 'fk_user_id');
}
LeadsModel:
public function propertyLead()
{
return $this->hasOne('App\Models\PropertyLead', 'fk_lead_id');
}
PropertyLeadModel:
public function attachments()
{
return $this->hasMany('App\Models\Attachments', 'fk_property_lead_id');
}
Now, I am using the Lazy Eager Loading of laravel to readData from dataBase, so far I am able to reach to PropertyLeadModel but I am not able to understand how to reach to attachment relation in the PropertyLeadModel,
$leads = User::find(Auth::user()->id)->leads->load('propertyLead');
so user gives me leads and leads gives me propertyLead but not able to understand how to reach more down to Attachments in the propertyLeadModel.
Please, help me to understand.
Thank you.

You can load nested relationships using "dot" notation.
$user = Auth::user();
$user->load('leads.propertyLead.attachments');
// see all relationships loaded
dd($user);
Since leads and attachments are "many" relationships, they will be Collections you have to iterate through to access any particular instance.

Try this, not tested
$leads = Auth::user()->leads->load('propertyLead', 'propertyLead.attachments');

Try this
User::with('leads.propertyLead.attachments')->where('id',Auth::user()->id)->first();

Related

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

How can I link 2 existing models in my laravel code?

I have two models customer and orders. They are already fecthed separately
$customers = customer::all();
$orders = orders::all();
customerID=1 has orderID : 1, 2,4 customerID=2 has orderID : 3,5,9
They are related (hasMany, belongsTo) but the problem is inside my for a certain reason they are separated but I want to send them as response in API using toJson or ToArray as one data having the orders nested to their correct customers.
How can I achieve that linking to have at the end one variable $customersWithOrders that should be transformed to JSON ?
I am using laravel 5.5
I don't know what the context is. Defining relationships as other answers mentioned is a good solution.
In addition, I recently read a pretty good article about this specific scenario.
So you can also do something like this, if you have already retrieved customers and orders:
$customers = Customer::all();
$orders = Order::all();
return $customers->each(function ($customers) use ($orders) {
$customer->setRelation('orders', $orders->where('customer_id', $customer->id));
});
If you already have a relation you just use it. For example, in model Customer.php:
public function orders()
{
return $this->hasMany(Order::class);
}
Then you'd get customer orders by calling $customer->orders
If you already have defined relations, you can simply fetch data with eager loading
// in customer model
public function orders()
{
return $this->hasMany(orders::class, 'orderID');
}
// in controller
$customersWithOrders = customer::with('orders')->get();
return response()->json(['customersWithOrders' => $customersWithOrders]);
// in js
for (let customer in response.customersWithOrders){
let orders = customer.orders
}

Laravel Eloquent relationship efficency

When I declare a relationship in a model, for example:
class Post extends Model
{
public function comments()
{
return $this->hasMany(Comment::class);
}
}
Are comments retrieved from the database the moment I retrieve the Post instance or the moment I write
$post->comments;
?
The answers so far solve this problem in pieces, but not very clearly so allow me to help. To answer your question bluntly, creating a Post instance does not also load associated comments.
Here is why:
When you define an Eloquent relationship, you are basically attaching a whole new 'query' method to your object and so it won't actually be executed unless you call it.
As a simple example we have Car:
class Car {
public $color;
public function __construct() {
$this->color = 'blue';
}
public function makeRed() {
$this->color = 'red';
return $this;
}
}
In this example, the instantiated Car will only have one property, color. This car will be blue unless you call the makeRed() method and change it. It does not compute both options simultaneously expecting that you may decide to change it's color.
So to relate that back to the Eloquent relationship, the comments method returns a relationship object, but only if the method is called on the Post object. Up until that point, your Post object will not automatically call it's own methods. Basically, don't worry about an object becoming large with a ton of methods as these methods only contribute to object size significantly if they are actually called.
If you wish for comments to be loaded with your Post immediately, eager loading the initial query will allow this by:
$post = Post::with('comments')->findOrFail('post_id');
Otherwise, the following would give you the comments for a given post:
$post = Post::findOrFail('post_id');
$post->comments;
Please see the Laravel documentation on Eager Loading for more information.
Hope this helps!
The answer its simple:
$post->comments() returns the relationship object
$post->comments returns the result of the relationship
So the moment you do $post->comments that means fetch relationship and execute query, therfore returns relational database results.
They are retrieved when you ask for it, i.e $post->comments. If you want to eager load them, you can write Post::with('comments')->get(). Check out documentation. It explains eager loading and the N+1 problem.
From the docs:
When accessing Eloquent relationships as properties, the relationship data is "lazy loaded". This means the relationship data is not actually loaded until you first access the property. However, Eloquent can "eager load" relationships at the time you query the parent model. Eager loading alleviates the N + 1 query problem.

Limits to multiple Eloquent relationships ?

I have a number of different roles, who are all essentially "users" in my laravel app.
I'm having a problem with eloquent relationships where I can easily get one of the relationships, in the context of the user as a car owner, but when I try get another relationship in the context of a (different) user as a maintenance manager, I get this error: Cannot redeclare class App\Models\User.
serviceAgreement model
public function manager()
{
return $this->belongsTo('carfreak\Models\User','manager_id','id');
}
Works fine:
$managers = $owner->serviceAgreement()->get();
produces error
$managers = $owner->serviceAgreement()->with('manager')->get();
I'm thinking the problem lies somewhere in how I've written my relationship - my referring to the \Models\User. I've tried to refer to the logged in user, but it (a) doesn't make sense for this application and (b) doesn't work anyway.
return $this->belongsTo(Auth::User(),'manager_id','id');
Some pointers please?
That is happen because the App\Models\User class is declared at least 2 times. Change
public function manager()
{
return $this->belongsTo('carfreak\Models\User','manager_id','id');
}
to
public function manager()
{
return $this->belongsTo('App\Models\User','manager_id','id');
}
You should go fine now.
Hope it helps.

Laravel - Retrieve the inverse of a many-to-many polymorphic relation (with pagination)

after some digging I still could not find any solid way to retrieve the inverse of a many-to-many polymorphic relation that allows mixed models results.
Please consider the following:
I have several models that can be "tagged". While it is trivial to retrieve for example $item->tags, $article->tags and the inverse with $tag->articles and $tag->items I have no easy way to do something like $tag->taggables to return both articles and items in the same collection. Things get even bumpier as I need to use pagination/simple pagination to the query.
I have tried a few workarounds but the best I could put together still looks crappy and limited. Basically:
I queried the DB once per "taggable";
put all in a single big collection;
passed the collection to a phpleague/fractal transformer (my API uses it) that returns different json values depending on the parsed models.
The limits of this approach is that building a pagination is a nightmare and fractal "include" options can't be used out of the box.
Can anyone help me? I'm currently using Laravel 5.1.
There is not much magic in my current code. Faking and simplifying it to make it short:
From the api controller:
$tag = Tag::findOrDie($tid);
$articles = $tag->cms_articles()->get();
$categories = $tag->cms_categories()->get();
$items = $tag->items()->simplePaginate($itemsperpage);
$taggables = Collection::make($articles)->merge($categories);
// Push items one by one as pagination would dirt the collection struct.
foreach ($items as $item) {
$taggables->push($item);
}
return $this->respondWithCollection($taggables, new TaggableTransformer);
Note: using simplePaginate() is there only because I would like all articles and categories to be shown on first page load while the number of items are so many that need pagination.
From the Transformer class:
public function transform($taggable)
{
switch (get_class($taggable)) {
case 'App\Item':
$transformer = new ItemTransformer;
break;
case 'App\CmsArticle':
$transformer = new CmsArticleTransformer;
break;
case 'App\CmsCategory':
$transformer = new CmsCategoryTransformer;
break;
}
return $transformer->transform($taggable);
}
Please consider that the other transformers are simply returning arrays of data about the models they correlate with. If you use Fractal you would easily spot that nested "included" models would not be applied.
Nothing fancy for the Tag model:
class Tag extends Model
{
protected $morphClass = 'Tag';
protected $fillable = array('name', 'language_id');
public function cms_articles() {
return $this->morphedByMany('App\CmsArticle', 'taggable');
}
public function cms_categories() {
return $this->morphedByMany('App\CmsCategory', 'taggable');
}
public function items() {
return $this->morphedByMany('App\Item', 'taggable');
}
// Would love something like this to return inverse relation!! :'(
public function taggables() {
return $this->morphTo();
}
}
I am also considering the option to do 3 separate calls to the API to retrieve articles, categories and items in three steps. While in this particular scenario this might make sense after all, I would still need to deal with this particular inverse relation headache with another part of my project: notifications. In this particular case, notifications would have to relate to many different actions/models and I would have to retrieve them all in batches (paginated) and sorted by model creation date...
Hope this all makes sense. I wonder if a completely different approach to the whole inverse "polymorphic" matter would help.
Kind regards,
Federico
Ah yes. I was down your path not all that long ago. I had the same nightmare of dealing with resolving the inverse of the relationship of polymorphic relationships.
Unfortunately polymorphic relationships haven't been given much attention in the Laravel ecosystem. From afar they look like unicorns and rainbows but soon you're fighting things like this.
Can you post an example of a $thing->taggable for a better picture? Think it may be solvable with a dynamic trait + accessor magic.

Resources