I wondered if anyone could offer me some advice on refactoring the below function to reduce the number of database queries? Alternatively, there may be a completely different way of achieving this using Laravel.
I am trying to calculate the P&L for a Job, which is made up of Products, which are made up of Components:
public function jobProfitAndLoss($id)
{
$products_in_job = DB::table('job_product')
->where('job_id', $id)
->get();
$total_price = 0.0000;
$total_cost = 0.0000;
foreach ($products_in_job as $row) {
$total_price = $total_price + ($row->quantity)*($row->price);
$product_id = $row->product_id;
$components_in_product = DB::table('components')
->where('product_id', $product_id)
->get();
foreach ($components_in_product as $component) {
$total_cost = $total_cost + ($component->cost)*($row->quantity);
}
}
return $total_price-$total_cost;
}
Products have Components -
https://www.dropbox.com/s/ncnij8dnh99sb9v/Screenshot%202016-04-09%2015.22.26.png?dl=0
Components belong to Products -
https://www.dropbox.com/s/3dx6u30gbod2rv4/Screenshot%202016-04-09%2015.23.43.png?dl=0
Jobs have many Products -
https://www.dropbox.com/s/q179t0knd7y8z4k/Screenshot%202016-04-09%2015.24.11.png?dl=0
You will see here that there are some of the same queries being executed multiple times, which I am not sure how to avoid in this situation -
https://www.dropbox.com/s/xonbtx9cdqvq1wd/Screenshot%202016-04-09%2015.33.07.png?dl=0
Any help is much appreciated.
Edit: It seems that you aren't using models. If you have not done so, create models for your database entries. You will need to use the protected $table attribute for job_product as Eloquent may not be able to automatically convert your class name into the correct table name.
First of all, set up relations if you have not done so. For example, under Job.php, include the Products relation:
public function products() {
return $this->hasMany(App\Products::class); // Assuming App is the namespace
}
Now, instead of using a Fluent query for $components_in_product, you are able to directly do $components_in_product = $products_in_job->products;. However, this still leads to N+1 queries.
As a result, take a look at this: https://laravel.com/docs/5.2/eloquent-relationships#eager-loading
$books = App\Book::with('author')->get();
foreach ($books as $book) {
echo $book->author->name;
}
For this operation, only two queries will be executed:
select * from books
select * from authors where id in (1, 2, 3, 4, 5, ...)
Therefore, change $products_in_job to an Eloquent query and add ->with('products') to the query.
Related
I have an Orders table that has relations to a Movements table, and im constantly doing things like this to calculate several common values for each order:
$warehouse = 7;
$order = Order::find(16111);
$entries = Movement::selectRaw("SUM(gross) AS total_gross")
->selectRaw("SUM(net) AS total_net")
->selectRaw("SUM(qty) AS total_qty")
->where('order_id', $order->id)
->where('to_id', $warehouse)
->first();
$exits = Movement::selectRaw("SUM(gross) AS total_gross")
->selectRaw("SUM(net) AS total_net")
->selectRaw("SUM(qty) AS total_qty")
->where('order_id', $order->id)
->where('from_id', $warehouse)
->first();
is it possible to create a custom function to just query the DB doing something like this:
$warehouse = 7;
$entries = Order::find(16111)->entries($warehouse);
$exits = Order::find(16111)->exits($warehouse);
If so how can it be done?
Thanks for your help...
Absolutely. What you are looking for is called local Query Scopes; it allows you to avoid repeating complexe queries in your code.
Local scopes allow you to define common sets of query constraints that you may easily re-use throughout your application.
Write your local query scope in your model and you'll never have to repeat this code again (DRY principle).
Here's an example to give you an idea, you'll need to tweak it to your needs.
In your Order model:
public function scopeEntries($query)
{
$warehouse = $this->warehouse; // Take advantage of Eloquent wherever you can
return $query->movements()->selectRaw("SUM(gross) AS total_gross")
->selectRaw("SUM(net) AS total_net")
->selectRaw("SUM(qty) AS total_qty")
->where('to_id', $warehouse->id);
}
public function scopeExits($query)
{
$warehouse = $this->warehouse; // Take advantage of Eloquent wherever you can
return $query->movements()->selectRaw("SUM(gross) AS total_gross")
->selectRaw("SUM(net) AS total_net")
->selectRaw("SUM(qty) AS total_qty")
->where('from_id', $warehouse->id)
->where('to_id', $warehouse->id);
}
Now in your code, you will be able to simply call $order->entries()->first() to retrieve the first entry but you can also call $order->exits()->get() to retrieve all exits.
I have the following relationship:
A venue has many offers
A offer has many orders
I have the following Eloquent model to represent this:
class Venue {
public function orders()
{
return $this->hasManyThrough(Order::class, Offer::class);
}
}
I want to determine the total number of orders for venues with location_id = 5 using Laravel's Eloquent model.
The only way I managed to do this is as follows:
$venues = Venue::where('location_id', 5)->with('orders')->get();
$numberOfOrders = 0;
foreach($venues as $venue) {
$numberOfOrders += $venue->orders->count();
}
dump($numberOfOrders); // Output a single number (e.g. 512)
However, this is obviously not very efficient as I am calculating the count using PHP instead of SQL.
How can I do this using Eloquent model alone.
You can use Eloquent. As of Laravel 5.3 there is withCount().
In your case you will have
$venues = Venue::where('location_id', 5)->with('orders')->withCount('orders')->get();
Then access it this way
foreach ($venues as $venue) {
echo $venue->orders_count;
}
Can find reference here: https://laravel.com/docs/5.3/eloquent-relationships#querying-relations
$venues = Venue::with([
'orders' => function ($q) {
$q->withCount('orders');
}
])->get();
then use it this way for getting single record
$venues->first()->orders->orders_count();
Alternatively, you can use this way too for collections
foreach($venues as $venue)
{
echo $venue->order_count;
}
If you are using Laravel 5.3 or above you can use withCount.
If you want to count the number of results from a relationship without
actually loading them you may use the withCount method, which will
place a {relation}_count column on your resulting models. For example:
$venues = Venue::withCount(['orders'])->get;
foreach ($venues as $venue) {
echo $venue->orders_count;
}
You can read more about withCount in the Laravel Documentation.
If you are using lower than 5.3, you can make a custom relation on your Venue model:
public function ordersCount()
{
return $this->belongsToMany('App\Models\Order')
->selectRaw('venue_id, count(*) as aggregate_orders')
->groupBy('venue_id');
}
public function getOrderCount()
{
// if relation is not loaded already, let's do it first
if (!array_key_exists('ordersCount', $this->relations)) {
$this->load('ordersCount');
}
$related = $this->getRelation('ordersCount')->first();
// then return the count directly
return ($related) ? (int) $related->aggregate_orders : 0;
}
which can then be used as: Venue::with('ordersCount');. The benefit of this custom relation is you only are querying the count rather than the querying all of those relations when they are not necessary.
I want to limit related records from
$categories = Category::with('exams')->get();
this will get me exams from all categories but what i would like is to get 5 exams from one category and for each category.
Category Model
public function Exams() {
return $this->hasMany('Exam');
}
Exam Model
public function category () {
return $this->belongsTo('Category');
}
I have tried couple of things but couldnt get it to work
First what i found is something like this
$categories = Category::with(['exams' => function($exams){
$exams->limit(5);
}])->get();
But the problem with this is it will only get me 5 records from all categories. Also i have tried to add limit to Category model
public function Exams() {
return $this->hasMany('Exam')->limit(5);
}
But this doesnt do anything and returns as tough it didnt have limit 5.
So is there a way i could do this with Eloquent or should i simply load everything (would like to pass on that) and use break with foreach?
There is no way to do this using Eloquent's eager loading. The options you have are:
Fetch categories with all examps and take only 5 exams for each of them:
$categories = Category::with('exams')->get()->map(function($category) {
$category->exams = $category->exams->take(5);
return $category;
});
It should be ok, as long as you do not have too much exam data in your database - "too much" will vary between projects, just best try and see if it's fast enough for you.
Fetch only categories and then fetch 5 exams for each of them with $category->exams. This will result in more queries being executed - one additional query per fetched category.
I just insert small logic inside it which is working for me.
$categories = Category::with('exams');
Step 1: I count the records which are coming in response
$totalRecordCount = $categories->count()
Step 2: Pass total count inside the with function
$categories->with([
'exams' => function($query) use($totalRecordCount){
$query->take(5*$totalRecordCount);
}
])
Step 3: Now you can retrieve the result as per requirement
$categories->get();
In this scenario I have a Ticket model and a TicketReply model.
I can grab all replies to a ticket with $ticket->replies.
Considering status 1 or 3 of a ticket or reply means its state is open/unresolved, this is how I currently find the total of tickets open the Eloquent way.
$tickets = Ticket::all();
$tickets_open = 0;
foreach($tickets as $t)
{
$tickets_open++;
if(($t->replies()->first()->status == 2) || ($t->status == 2))
{
$tickets_open--;
}
}
return $tickets_open;
Is there a more efficient way of doing this with Eloquent?
Since as it is a query will run for each iteration.
If there is not, I can convert $tickets to an array and iterate it.
Update:
$t->replies()->first()->status was causing the 1+N with or without eager loading.
I changed to $t->replies->first()->status and 1+N is gone. A rookie mistake I believe.
You can use Eager Loading to reduce the number of queries.
$tickets = Ticket::with('replies')->get();
Doing this, instead of running 1+N queries, you will end up running two SQL queries:
select * from Tickets
select * from TicketReplies where ticket_id in (1, 2, 3, 4, 5, ...)
Though it's a bit confusing to me but I believe you may try something like this:
$openTickets = Reply::where('status', 2)->count();
Also maybe something like this:
$tickets = Ticket::whereHas('replies', function($query) {
$query->where('status', 2);
})->with('replies')->get();
Then count:
$t = 0;
$tickets->fetch('replies')->each(function($i) use (&$t){
$t += count($i);
});
dd($t); // total
I could be wrong but I guessed it could be done like this.
P/S:Please let me know the update.
I am working on an application where part of it needs to search through a number of different fields on the same model with AND - AKA find age whereBetween $from and $to AND where gender is $gender. Where I am getting lost is this model has a many to many relationship with Category and I need to filter by category in the same query. I am trying to do this in one query because it needs to be pretty fast.
Here is what I have so far:
$categories = Input::get('categories');
$query = Program::with('categories')->whereIn('category', $categories)->query();
if ($ages = Input::get('ages')) {
$query->whereBetween('age',array($from,$to));
}
if ($gender = Input::get('gender')) {
$query->where('gender','like', $gender);
}
// Executes the query and fetches it's results
$programs = $query->get();
I have put this together from so many different sources that I would like to know if this even works, or if it is the most efficient method possible. There is of course a table programs, a table categories, and a table program_category with columns id, program_id, and category_id.
Thanks in advance for your help!
So, in the end figured it out:
$query = Program::whereHas('categories', function($q) use ($categories)
{
$q->whereIn('categories.id', $categories);
});
'categories' is the name of the relationship function on my Program model. $categories is my array of category ids. Thanks again for your help.
This will work if fields are available in the right table as you queried for and you may write it like this:
$categories = Input::get('categories');
$query = Program::with('categories')->whereIn('category', $categories);
// declare $from and $to if not available in the current scope
if ($ages = Input::get('ages')) $query->whereBetween('age', array($from,$to));
if ($gender = Input::get('gender')) $query->where('gender', $gender);
// Executes the query and fetches it's results
$programs = $query->get();