Laravel reducing queries - php

I use Laravel Eloquent and I have this code:
<?php
$bulk = Bulk::where('id', 'bulkid1');
echo $bulk->info;
foreach($bulk->models as $model) {
echo $model->info;
$lastreport = $model->reports()->getQuery()->orderBy('created_at', 'desc')->first();
echo $lastreport->message;
}
?>
What I want to achieve is that the $lastreport is preloaded. In this piece of code, the query will be executed too many times (every bulk has 1000 models, resulting in 1000 subqueries).
While in plain sql I could just do:
SELECT * FROM bulk
LEFT JOIN models ON bulk.id = models.bulk_id
LEFT JOIN ( SELECT *, MAX(created_at) AS created_at
FROM
reports
GROUP BY
model_id )
lastreport ON models.id = lastreport.model_id
WHERE bulk.id = 'bulkid1'
Database pseudocode:
TABLE bulks
id, info
TABLE models
id, bulk_id, info
TABLE reports
id, model_id, message

This is the N+1 selects problem The laravel solution for this problem is eager loading
In your case you'd do:
<?php
$bulk = Bulk::with(['models', 'models.reports' => function ($query) {
return $query->orderBy('created_at', 'desc');
}
])->where('id', 'bulkid1')->first();
echo $bulk->info;
foreach($bulk->models as $model) {
echo $model->info;
$lastreport = $model->reports->first();
echo $lastreport->message;
}
This should ensure that (a) all models are loaded with only 1 additional query and (b) all model reports are loaded with another additional query. The downside with this is that there are more data being loaded than necessary because of the orderBy clause which can't really be represented as a query time condition.

Related

withExists() or withCount() for nested relationship (Many To Many and One To Many) in Laravel Eloquent

I have the following relationships in models:
Product.php
public function skus()
{
return $this->belongsToMany(Sku::class);
}
Sku.php
public function prices()
{
return $this->hasMany(Price::class);
}
I need to get an attribute indicating whether a product has at least one price or not (in the extreme case, just the number of prices).
Product::withExists('sku.prices') or Product::withCount('sku.prices')
I know about this repository https://github.com/staudenmeir/belongs-to-through, but I prefer to use complex query once
UPDATE: I have already written a sql query for this purpose, but I don't know how to do it in Laravel:
SELECT
*,
EXISTS (SELECT
*
FROM prices
INNER JOIN skus
ON prices.sku_id = skus.id
INNER JOIN product_sku
ON skus.id = product_sku.sku_id
WHERE products.id = product_sku.product_id
) AS prices_exists
FROM products
Here you can get at least one record
$skuPrice = Sku::with('prices')
->has('prices', '>=', 1)
->withCount('prices')
->get();

Laravel query builder add complex query result

I have three models with the following hierarchy :
User
id
....some other properties
Journey
id
user_id
budget
....some other properties
Confirmation
id
journey_id
user_id
....some other properties
I have a HasMany from User to Journey, a HasMany from Journey to Confirmation.
I want to get the sum for a column of the journeys table by going through the confirmations table but I cannot create an intermediate HasManyThrough relation between User and Journey by using Confirmation.
I have tried to do
public function journeysMade(): HasManyThrough
{
return $this->hasManyThrough(Journey::class, Confirmation::class);
}
// And after,
User::with(...)->withSum('journeysMade','budget')
But it was not possible because the relations are not adapted.
With hindsight, the sql query I want to translate would look like
select coalesce(sum(journeys.budget), 0) as income
from journeys
inner join confirmations c on journeys.id = c.journey_id
where c.user_id = ? and c.status = 'finalized';
How can I implement this query considering how I will use my query builder :
$driversQueryBuilder = User::with(['profile', 'addresses']); // Here
$pageSize = $request->input('pageSize', self::DEFAULT_PAGE_SIZE);
$pageNumber = $request->input('pageNumber', self::DEFAULT_PAGE_NUMBER);
$driversPaginator = (new UserFilterService($driversQueryBuilder))
->withStatus(Profile::STATUS_DRIVER)
->withCountry($request->input('country'))
->withSex($request->input('sex'))
->withActive($request->has('active') ? $request->boolean('active') : null)
->get()
->paginate(perPage: $pageSize, page: $pageNumber);
return response()->json(['data' => $driversPaginator]);
The reason why I want to get a builder is because UserFilterService expects a Illuminate\Database\Eloquent\Builder.
Do you have any idea about how I can solve this problem ?
Not 100% sure what exactly you want to sum, but I think you need the following query
$user->whereHas('journeys', function($query) {
$query->whereHas('confirmations', function($subQuery) {
$subQuery->sum('budget);
}
});
If you the above query isn't summing the budget you need, you just add another layer of abstraction with whereHas methods to get exactly what you need. Hope this helps!
EDIT:
$user->whereHas('confirmations', function($q) {
$q->withSum('journeys', 'budget')->journeys_sum_budget;
}

Laravel eager loading with limit

I have two tables, say "users" and "users_actions", where "users_actions" has an hasMany relation with users:
users
id | name | surname | email...
actions
id | id_action | id_user | log | created_at
Model Users.php
class Users {
public function action()
{
return $this->hasMany('Action', 'user_id')->orderBy('created_at', 'desc');
}
}
Now, I want to retrieve a list of all users with their LAST action.
I saw that doing Users::with('action')->get();
can easily give me the last action by simply fetching only the first result of the relation:
foreach ($users as $user) {
echo $user->action[0]->description;
}
but I wanted to avoid this of course, and just pick ONLY THE LAST action for EACH user.
I tried using a constraint, like
Users::with(['action' => function ($query) {
$query->orderBy('created_at', 'desc')
->limit(1);
}])
->get();
but that gives me an incorrect result since Laravel executes this query:
SELECT * FROM users_actions WHERE user_id IN (1,2,3,4,5)
ORDER BY created_at
LIMIT 1
which is of course wrong. Is there any possibility to get this without executing a query for each record using Eloquent?
Am I making some obvious mistake I'm not seeing? I'm quite new to using Eloquent and sometimes relationship troubles me.
Edit:
A part from the representational purpose, I also need this feature for searching inside a relation, say for example I want to search users where LAST ACTION = 'something'
I tried using
$actions->whereHas('action', function($query) {
$query->where('id_action', 1);
});
but this gives me ALL the users which had had an action = 1, and since it's a log everyone passed that step.
Edit 2:
Thanks to #berkayk looks like I solved the first part of my problem, but still I can't search within the relation.
Actions::whereHas('latestAction', function($query) {
$query->where('id_action', 1);
});
still doesn't perform the right query, it generates something like:
select * from `users` where
(select count(*)
from `users_action`
where `users_action`.`user_id` = `users`.`id`
and `id_action` in ('1')
) >= 1
order by `created_at` desc
I need to get the record where the latest action is 1
I think the solution you are asking for is explained here http://softonsofa.com/tweaking-eloquent-relations-how-to-get-latest-related-model/
Define this relation in User model,
public function latestAction()
{
return $this->hasOne('Action')->latest();
}
And get the results with
User::with('latestAction')->get();
I created a package for this: https://github.com/staudenmeir/eloquent-eager-limit
Use the HasEagerLimit trait in both the parent and the related model.
class User extends Model {
use \Staudenmeir\EloquentEagerLimit\HasEagerLimit;
}
class Action extends Model {
use \Staudenmeir\EloquentEagerLimit\HasEagerLimit;
}
Then simply chain ->limit(1) call in your eager-load query (which seems you already do), and you will get the latest action per user.
My solution linked by #berbayk is cool if you want to easily get latest hasMany related model.
However, it couldn't solve the other part of what you're asking for, since querying this relation with where clause would result in pretty much the same what you already experienced - all rows would be returned, only latest wouldn't be latest in fact (but latest matching the where constraint).
So here you go:
the easy way - get all and filter collection:
User::has('actions')->with('latestAction')->get()->filter(function ($user) {
return $user->latestAction->id_action == 1;
});
or the hard way - do it in sql (assuming MySQL):
User::whereHas('actions', function ($q) {
// where id = (..subquery..)
$q->where('id', function ($q) {
$q->from('actions as sub')
->selectRaw('max(id)')
->whereRaw('actions.user_id = sub.user_id');
})->where('id_action', 1);
})->with('latestAction')->get();
Choose one of these solutions by comparing performance - the first will return all rows and filter possibly big collection.
The latter will run subquery (whereHas) with nested subquery (where('id', function () {..}), so both ways might be potentially slow on big table.
Let change a bit the #berkayk's code.
Define this relation in Users model,
public function latestAction()
{
return $this->hasOne('Action')->latest();
}
And
Users::with(['latestAction' => function ($query) {
$query->where('id_action', 1);
}])->get();
To load latest related data for each user you could get it using self join approach on actions table something like
select u.*, a.*
from users u
join actions a on u.id = a.user_id
left join actions a1 on a.user_id = a1.user_id
and a.created_at < a1.created_at
where a1.user_id is null
a.id_action = 1 // id_action filter on related latest record
To do it via query builder way you can write it as
DB::table('users as u')
->select('u.*', 'a.*')
->join('actions as a', 'u.id', '=', 'a.user_id')
->leftJoin('actions as a1', function ($join) {
$join->on('a.user_id', '=', 'a1.user_id')
->whereRaw(DB::raw('a.created_at < a1.created_at'));
})
->whereNull('a1.user_id')
->where('aid_action', 1) // id_action filter on related latest record
->get();
To eager to the latest relation for a user you can define it as a hasOne relation on your model like
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
class User extends Model
{
public function latest_action()
{
return $this->hasOne(\App\Models\Action::class, 'user_id')
->leftJoin('actions as a1', function ($join) {
$join->on('actions.user_id', '=', 'a1.user_id')
->whereRaw(DB::raw('actions.created_at < a1.created_at'));
})->whereNull('a1.user_id')
->select('actions.*');
}
}
There is no need for dependent sub query just apply regular filter inside whereHas
User::with('latest_action')
->whereHas('latest_action', function ($query) {
$query->where('id_action', 1);
})
->get();
Migrating Raw SQL to Eloquent
Laravel Eloquent select all rows with max created_at
Laravel - Get the last entry of each UID type
Laravel Eloquent group by most recent record
Laravel Uses take() function not Limit
Try the below Code i hope it's working fine for u
Users::with(['action' => function ($query) {
$query->orderBy('created_at', 'desc')->take(1);
}])->get();
or simply add a take method to your relationship like below
return $this->hasMany('Action', 'user_id')->orderBy('created_at', 'desc')->take(1);

laravel: convert many to many (but only one related item) mysql query to a laravel query

Hi I'm having trouble converting a mysql query that I've been working on into a laravel eloquent query and need some help.
I have a reservations table which links to a product table with a many to many relationship. I want to pull all the reservations and just the first product it finds regardless of how many products are related to the reservation.
Here's my sql:
SELECT reservations.id,
reservations.play_date,
reservations.group_size,
reservations.status,
reservations.event_title,
t4.product_id,
t4.id AS link_id,
p1.name,
CONCAT_WS(" ", customers.first_name, customers.last_name, customers.group_name) AS customerName,
reservations.event_type
FROM reservations
LEFT JOIN customers ON reservations.customer_id = customers.id
LEFT JOIN
(SELECT *
FROM product_reservation AS t3
GROUP BY t3.reservation_id ) AS t4 ON t4.reservation_id = reservations.id
LEFT JOIN products AS p1 ON t4.product_id = p1.id
I can place this as a raw query but that produces an array with the result - I need to be able to create a query object so I can work with another module on the results
Is there an eloquent way of doing this - or how can I get this query to work in laravel?
Thank you
Yeah, you can use Eloquent relationships. They would look something like this...
class Reservation extends Eloquent
{
public function products()
{
return $this->belongsToMany('Product');
}
}
class Product
{
public function reservations()
{
return $this->belongsToMany('Reservation');
}
}
$reservations = Reservation::with(array('products', function($q) {
$q->take(1);
}))->get();
foreach($reservations as $reservation) {
echo $reservation->name;
foreach($reservation->products as $product) {
echo $product->description;
}
}

Counting related rows in a child table

I have been trying to do some queries and getting a count on related tables using eloquent.
Tables:
requests
contact (belongs to requests)
history (belongs to contact)
As such X number of requests each have Y number of contacts which in term each have Z number of histories
Using sql I can do something like this to get all the counts.
SELECT
id,
(
SELECT count(contact.id)
FROM contact
WHERE contact.requests_id = requests.id
) AS n_contact,
(
SELECT count(history.id)
FROM contact INNER JOIN history ON (history.contact_id = contact.id)
WHERE contact.requests_id = requests.id
) AS n_history
FROM requests;
But I am a bit lost when using eloquent to build queries. If for instance I was selecting all contacts for a given request at what point would I join/count the history? Or do I need to add in some accessor's into the relevant Models for these 3 tables?
public function getAllContacts($id) {
return Requests::where('requests.id', '=', $id)
->join('requests', 'contact.requests_id', '=', 'requests.id')
->select('contact.*', 'requests.name');
->get();
}
Thanks in advance for any help.
You can use helper relation for this, if you'd like to use Eloquent instead of manual joins:
// Request model
public function contactsCount()
{
return $this->hasOne('Contact')->selectRaw('request_id, count(*) as aggregate')->groupBy('request_id');
}
public function getContactsCountAttribute()
{
if ( ! array_key_exists('contactsCount', $this->relations)) $this->load('contactsCount');
return $this->getRelation('contactsCount')->aggregate;
}
The same would go for Contact model towards History model.
For counting far relation (Request -> History) you can use hasManyThrough relation with a little adjustment.
This way you can eager load those aggregates for multiple models without n+1 issue, nice and easy:
$requests = Request::with('contactsCount', 'contacts.historyCount')->get();
// 1 query for reuqests, 1 query for contacts count and 2 queries for contacts.historyCount
// example output
$requests->first()->contactsCount; // 17
$requests->first()->contacts->first()->historyCount; // 5
/* Make Relation in the Request Model */
public function contacts()
{
return $this->hasMany('App\Model\Contact', 'request_id', 'id');
}
/* use withCount() to get the total numner of contacts */
public function getAllContacts($id) {
return Requests::with('contacts')
->withCount('contacts')
->find($id);
}

Categories