Eliminating Duplicate Queries with Eager Loading - php

I am trying to eliminate unnecessary queries on my site but am struggling to wrap my head around Eager Loading and Lazy Loading. All users on my site have listings, and listings have multiple users. They are connected through the table listing_users. Every listing then has one "order" associated with it. Here is the user model:
User Model:
public function listings(){
return $this->belongsToMany(Listing::class)->withPivot('role_id');
}
Listing Model:
public function order(){
return $this->hasOne(Order::class)->first();
}
My current dashboard is loaded by calling this viewListings in the UserController:
public function viewListings(){
$user = Auth::user();
$listings = $user->listings()->orderBy('created_at','desc')->get();
return view('user.listings', compact('listings'));
}
The problem occurs in my blade view user.listings where I have a foreach loop for every listing and then call each order as well. I need a way to pass the listings to the page, with their related orders.
#foreach($listings as $listing)
#if($listing->order()->status == 'completed')
{{-- Display the listing details here --}}
#endif
#endforeach
Any advice into the above situation would be greatly appreciated! I'm sure there is a simple Laravel solution for this that I'm overlooking.

Try this:
Listing Model:
public function order(){
return $this->hasOne(Order::class); //without first()
}
UserController:
Here we use method with('order') for eager loading Order model for each Listing models retrieved by query. So now in your blade will not be unnecessary queries.
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.
public function viewListings(){
$user = Auth::user();
$listings = $user->listings()->orderBy('created_at','desc')
->with('order')->get();//added with('order')
return view('user.listings', compact('listings'));
}
user.listings:
You should use order without () if you need retrieve your model. So if you want modify order then use it with () as query builder, and add further constraints like where, orderBy etc. and in the end add first().
Here you can understand why we removed first() from hasOne above.
#foreach($listings as $listing)
#if($listing->order->status == 'completed')
{{-- order instead of order() --}}
{{-- Display the listing details here --}}
#endif
#endforeach

Related

Eloquent Eager Loading "Property [service_code] does not exist on this collection instance"

I cannot figure out how eager loading works with the following example. This is my current DB with two tables, Quotes: where the general information is stored, and QuotesDetails: where details of each Quote in Quotes is stored.
In models I have the following structures:
Quotes.php
class Quotes extends Model
{
public function quotesdetails()
{
return $this->hasMany('App\QuotesDetails', 'quoteid', 'id');
}
}
and QuotesDetails.php with the following model:
class QuotesDetails extends Model
{
public function quotes()
{
return $this->belongsTo('App\Quotes', 'id', 'quoteid');
}
}
I used hasMany (in Quotes) because each quote can have/display 3-4 quotesdetails.
In my controller im using the following query:
$returnquotes = Quotes::with('quotesdetails')->where('id', '=', $quoteid)->get();
In my view im using the following structure:
#foreach ($returnquotes as $quotes)
{{$quotes->shipcity }}
{{$quotes->quotesdetails->service_code }}
#endforeach
shipcity displays the information with no problems, but service_code is not displayed and gives error.
Honestly I believe it has to work with this schema but I cannot figure out why is not working. My thoughts:
in controller: using "with('quotesdetails')" in controller it must establish the relation that appears in Quotes.php with name -> quotedetails that hasmany registries associated in QuotesDetails.php
in view: using "$quotes->quotesdetails->service_code" must retrieve the service_code associated to the quotes table (i'm using foreach because quotesdetails can have multiple registries per quote)
in model: Im using "hasMany" for Quotes.php because the dependent table is QuotesDetails and "belongsTo" in QuotesDetails.php for the inverse reason.
any help to understand the logic of eager loading with eloquent and laravel appreciated.
$quotes->quotesdetails is a collection itself (has many) so you need to iterate over it using another foreach:
#foreach ($returnquotes as $quotes)
{{$quotes->shipcity }}
#foreach ($quotes->quotesdetails as $quotedetail)
{{$quotedetail->service_code }}
#endforeach
#endforeach

Laravel retrieves too many model on a simple paginate query with loaded relations

I have a simple database with couple of tables. orders - the main table, about 16_000 records. Each order has payments. The order_payments table is about 17_000 records. Also each could have zero or more travellers in a order_tourists table - 33_000 rows.
The Order model looks like this
namespace App;
class Order extends \Illuminate\Database\Eloquent\Model
{
public function payments()
{
return $this->hasMany('App\OrderPayment');
}
public function tourists()
{
return $this->hasMany('App\OrderTourist');
}
}
I have a simple task to display a paginated list of all orders. Each item in this list should display some order's information (from the orders table), the sum of all payments and names of all travellers.
The first that came to my mind was something like this.
Get all orders with payments and travellers and pass this data to a view
Route::get('search_offers', static function () {
$orders = \App\Order::query()->with(['payments', 'tourists']);
return view('orders', [
'orders' => $orders->paginate(),
]);
});
And my orders.blade.php is pretty straightforward
<ol>
#foreach($orders as $order)
<li>
{{$order->request_id}}; {{ $order->payments->sum('amount') }}; {{ $order->tourists->implode('fullName', ', ') }}
</li>
#endforeach
</ol>
However, even the number of queries is very small I have a lot of manipulations with models. I add a custom hook to track all eloquent.* events and in first case there are more than 23_000 of such events.
Then I try to remove the loading of relations so for each order Laravel have to run a separate query to get all order's payments and travellers. I simply did like this in my controller
return view('orders', [
'orders' => \App\Order::query()->paginate(),
]);
So, I get more queries but page speed improves and a number of affected models significantly decreased
To track affected models I add a listner in a AppServiceProvider::boot method and output a AppServiceProvider::$hydratedModels value in a debugbar panel
Event::listen('eloquent.*', static function ($event) {
if (strpos($event, 'eloquent.retrieved') !== false) {
AppServiceProvider::$hydratedModels++;
}
});
So, my question how it possible that so many models are retrieved even if I display only 15 items. Looks like Laravel somehow get all of them, processed but retruns only those for a required page.
Right now it's not a big deal to run extra queries to fetch some data but I'm wondering maybe I'm doing something wrong and it's possible to load relations but do not cause Laravel to retrieve all models.
I found the issue. Thanks, #dparoli for a suggestion.
My models have a primary key as UUID. But I did not change a Model::$keyType so internally Laravel cast it into an integer and queries look like this.
Instead of a UUID string, I got integers as an order_id for related records.
So, I add protected $keyType = 'string'; and everything starts work as expected.
PS. Although, I'm wondering how it works before.

Merging two models into one database call - laravel

I have 2 models, that I am calling via all:
$blocks = Block::all();
$cats = BlockCategory::all();
A block has a category associated with it and I want to associate the data so that I can display all categories that have certain blocks. I hope I am explaining this correctly.
so I can call:
#foreach($cats as $cats)
{{$cats->title}}
#foreach($cats->blocks as $blocks)
{{$block->title}}
#endforeach
#endforeach
any ideas how to merge the data? I am basically building a menu if that helps.
You need to define relationship between blocks and block category. Make sure that your db table has a foreign key relationship.
Add following code to your block category model
public function blocks()
{
return $this->hasMany('App\Block');
}
Make sure your block model is inside app/ directory.
Now, just retrieve categories in your controller,
$categories = BlockCategory::all();
Then you can display the data like this:
#foreach($categories as $category)
{{$category->title}}
#foreach($category->blocks as $block)
{{$block->title}}
#endforeach
#endforeach
You have a few options on how to approach this.
Define your relation like this:
Model
public function blocks()
{
return $this->hasMany('App\Block');
}
Controller
Using with()
$categories = Blockcategory::with('blocks')->get();
Once you have defined the relationship you can call the blocks() method from your model to retrieve the blocks
Examples
$categories = Blockcategory::find(1)->blocks()->get();
// another way
$blocks = Blockcategory::blocks()->get();
Sagar's answer is fine, but it is always better to get your categories and their relationships in the same query. By just calling Blockcategory::all() you can get the relation in your blade file, but laravel has to do an additional query for each block in your for each loop. This can give performance issues when having 1000s of records.
Laravel relationships

Eager load ONE from Many to Many relationship

I have a many to many relationship for users and roles. A user can have multiple roles, but I only want with to grab the FIRST role.
Consider the following code:
User::with('roles')->get()
Works great for all roles, but I only want the first role.
I've set this up in my model but doesn't work:
public function role()
{
return $this->roles()->first();
}
How do I load with for only the first result?
You should be able to call first directly on the eager loaded relationship like this:
User::with(['roles' => function ($query) {
$query->first();
})->get();
first() actually executes the query and returns the results as a collection. Relationships must return a query builder, which can then be chained or executed, so using first() in a relationship won't work.
UPDATE
I realised you want to use role in with, so you need to create a relationship to do that. Create a new relationship on your User model (you can use any limit described in the docs, not just oldest()):
public function role()
{
return $this->hasOne('App\Role')->oldest();
}
And then you can use it in with:
$users = User::with('role')->get();

Laravel get a collection of relationship items

I'm stuck on this what seems like a simple task.
I have a User that has many Shops that have many Products..
I'm trying to get all the Products for a certain User.
This is working, and is returning the Shops with their Products
\Auth::user()->shops()->with('products')->get();
But I need a Collection of only the Products. I tried the following but it's messing up the Query
\Auth::user()->shops()->with('products')->select('products.*')->get();
Any idea what I'm doing wrong? Thank you!
You can use this :
\Auth::user()->shops()->with('products')->get()->pluck('products')->flatten();
if you don't want replicate, you can use ->unique()
If you want to directly work on a query (for performances):
Product::whereHas('shops', function($query){
$query->where('user_id', auth()->user()->id);
})->get();
what you need is a relationship between the User Model and the Product Model ..
this is possible using hasManyThrough relationship ..
USER MODEL
public function products()
{
return $this->hasManyThrough('App\Product', 'App\Shop')
}
now, you can access all the user's products with
Auth::user()->products;
In this case you can use lazy eager loading:
auth()->user()->load('shops.products');
To iterate over products:
#foreach (auth()->user()->shops as $shop)
#foreach ($shop->products as $product)
{{ $product->name }}
#endforeach
#endforeach
If you need just products:
Product::whereHas('shop', function ($q) {
$q->where('user_id', auth()->id());
})->get();
In both cases, you'll have the same number of queries to DB, so I'd use the first example in a real app.
Assuming that your product model has the shop id stored, try the following:
Products::whereIn('id_shop',
Shops::select('id')
->where('id_owner_user',Auth::user()->id)
->get())
->get()
It will retrieve a collection of products that belong to the list of shops which belong to the authenticated user

Categories