Laravel Spatie Query Builder - Wrong query when make join - php

I have a problem with packages spatie/Laravel-query-builder. I used this package for easy way to filter and sort my query but it's not like that :D
I trying to filter result who have two relation - shop and employee. In short, it wants to filter a list of store reports
Look, this is my code. When I use join method then in response receives data with incorrect ID. When comment join methods all it's okey but I need sorting by relation.
return ReportControlResource::collection(
QueryBuilder::for(Report::class)
->with(['employee', 'shop'])
->allowedFilters('shop.name', 'employee.name')
->join('employees', 'employees.id', '=', 'reports.employee_id')
->join('shops', 'shops.id', '=', 'reports.shop_id')
->allowedSorts(['employees.name'])
->get()
);
My relation in Report model:
public function shop(): BelongsTo
{
return $this->belongsTo(Shop::class);
}
public function employee(): BelongsTo
{
return $this->belongsTo(Employee::class);
}
Relation in Shop model:
public function reports()
{
return $this->hasMany(Report::class);
}
And in Employee model
public function reports()
{
return $this->hasMany(Report::class);
}
Do you have any ideas?
I noticed that the ID is being overwritten, but not shop_id and employee_id, but the ID why??
I think the problem is with the library itself. The creators did not take into account the reverse situation of joining tables as in my case.
Look at example from Doc:
$addRelationConstraint = false;
QueryBuilder::for(User::class)
->join('posts', 'posts.user_id', 'users.id')
->allowedFilters(AllowedFilter::exact('posts.title', null, $addRelationConstraint));
And my join
->join('employees', 'employees.id', '=', 'reports.employee_id')
But this join work like this
->join('employees', 'reports.id', '=', 'reports.employee_id')
But why? I checked the order in many ways, even disconnecting filtering and it did not change anything

I found solution for the problem. I had to select a column and now all works fine :-)
return EvidenceControlResource::collection(
QueryBuilder::for(EvidenceControl::class)
->allowedIncludes('employee', 'shop')
->select('evidence_controls.*', DB::raw('employees.id as employee_id'))
->join('employees', 'evidence_controls.employee_id', '=', 'employees.id')
->select('evidence_controls.*', DB::raw('shops.id as shop_id'))
->join('shops', 'evidence_controls.shop_id', '=', 'shops.id')
->allowedFilters('shop.name', 'employee.name')
->allowedSorts(['employees.name', 'shops.name'])
->get()
);
This is not clean code, but when create own class with implements Sort the code might look a lot better.
Thanks Guy's for help! Good luck! :-)

Related

laravel eloquent sort by relationship

I have 3 models
User
Channel
Reply
model relations
user have belongsToMany('App\Channel');
channel have hasMany('App\Reply', 'channel_id', 'id')->oldest();
let's say i have 2 channels
- channel-1
- channel-2
channel-2 has latest replies than channel-1
now, i want to order the user's channel by its channel's current reply.
just like some chat application.
how can i order the user's channel just like this?
channel-2
channel-1
i already tried some codes. but nothing happen
// User Model
public function channels()
{
return $this->belongsToMany('App\Channel', 'channel_user')
->withPivot('is_approved')
->with(['replies'])
->orderBy('replies.created_at'); // error
}
// also
public function channels()
{
return $this->belongsToMany('App\Channel', 'channel_user')
->withPivot('is_approved')
->with(['replies' => function($qry) {
$qry->latest();
}]);
}
// but i did not get the expected result
EDIT
also, i tried this. yes i did get the expected result but it would not load all channel if there's no reply.
public function channels()
{
return $this->belongsToMany('App\Channel')
->withPivot('is_approved')
->join('replies', 'replies.channel_id', '=', 'channels.id')
->groupBy('replies.channel_id')
->orderBy('replies.created_at', 'ASC');
}
EDIT:
According to my knowledge, eager load with method run 2nd query. That's why you can't achieve what you want with eager loading with method.
I think use join method in combination with relationship method is the solution. The following solution is fully tested and work well.
// In User Model
public function channels()
{
return $this->belongsToMany('App\Channel', 'channel_user')
->withPivot('is_approved');
}
public function sortedChannels($orderBy)
{
return $this->channels()
->join('replies', 'replies.channel_id', '=', 'channel.id')
->orderBy('replies.created_at', $orderBy)
->get();
}
Then you can call $user->sortedChannels('desc') to get the list of channels order by replies created_at attribute.
For condition like channels (which may or may not have replies), just use leftJoin method.
public function sortedChannels($orderBy)
{
return $this->channels()
->leftJoin('replies', 'channel.id', '=', 'replies.channel_id')
->orderBy('replies.created_at', $orderBy)
->get();
}
Edit:
If you want to add groupBy method to the query, you have to pay special attention to your orderBy clause. Because in Sql nature, Group By clause run first before Order By clause. See detail this problem at this stackoverflow question.
So if you add groupBy method, you have to use orderByRaw method and should be implemented like the following.
return $this->channels()
->leftJoin('replies', 'channels.id', '=', 'replies.channel_id')
->groupBy(['channels.id'])
->orderByRaw('max(replies.created_at) desc')
->get();
Inside your channel class you need to create this hasOne relation (you channel hasMany replies, but it hasOne latest reply):
public function latestReply()
{
return $this->hasOne(\App\Reply)->latest();
}
You can now get all channels ordered by latest reply like this:
Channel::with('latestReply')->get()->sortByDesc('latestReply.created_at');
To get all channels from the user ordered by latest reply you would need that method:
public function getChannelsOrderdByLatestReply()
{
return $this->channels()->with('latestReply')->get()->sortByDesc('latestReply.created_at');
}
where channels() is given by:
public function channels()
{
return $this->belongsToMany('App\Channel');
}
Firstly, you don't have to specify the name of the pivot table if you follow Laravel's naming convention so your code looks a bit cleaner:
public function channels()
{
return $this->belongsToMany('App\Channel') ...
Secondly, you'd have to call join explicitly to achieve the result in one query:
public function channels()
{
return $this->belongsToMany(Channel::class) // a bit more clean
->withPivot('is_approved')
->leftJoin('replies', 'replies.channel_id', '=', 'channels.id') // channels.id
->groupBy('replies.channel_id')
->orderBy('replies.created_at', 'desc');
}
If you have a hasOne() relationship, you can sort all the records by doing:
$results = Channel::with('reply')
->join('replies', 'channels.replay_id', '=', 'replies.id')
->orderBy('replies.created_at', 'desc')
->paginate(10);
This sorts all the channels records by the newest replies (assuming you have only one reply per channel.) This is not your case, but someone may be looking for something like this (as I was.)

Laravel OrderBy Nested Collection

I'm using a Roles package (similar to entrust). I'm trying to sort my User::all() query on roles.id or roles.name
The following is all working
User::with('roles');
This returns a Collection, with a Roles relation that also is a collection.. Like this:
I'm trying to get all users, but ordered by their role ID.
I tried the following without success
maybe because 'roles' returns a collection? And not the first role?
return App\User::with(['roles' => function($query) {
$query->orderBy('roles.id', 'asc');
}])->get();
And this
return App\User::with('roles')->orderBy('roles.id','DESC')->get();
None of them are working. I'm stuck! Can someone point me in the right direction please?
You can take the help of joins like this:
App\User::join('roles', 'users.role_id', '=', 'roles.id')
->orderBy('roles.id', 'desc')
->get();
Hope this helps!
You can make accessor which contains role id or name that you want to sort by.
Assume that the accessor name is roleCode. Then App\User::all()->sortBy('roleCode') will work.
Here's the dirty trick using collections. There might be a better way to achieve this(using Paginator class, I guess). This solution is definitely a disaster for huge tables.
$roles = Role::with('users')->orderBy('id', 'DESC')->get();
$sortedByRoleId = collect();
$roles->each(function ($role) use($sorted) {
$sortedByRoleId->push($role->users);
});
$sortedByRoleId = $sortedByRoleId->flatten()->keyBy('id');
You can sort your relations by using the query builder:
notice the difference with your own example: I don't set roles.id but just id
$users = App\User::with(['roles' => function ($query) {
$query->orderBy('id', 'desc');
}])->get();
See the Official Laravel Docs on Constraining Eager Loading
f you want to order the result based on nested relation column, you must use a chain of joins:
$values = User::query()->leftJoin('model_has_roles', function ($join)
{
$join>on('model_has_roles.model_id', '=', 'users.id')
->where('model_has_roles.model_type', '=', 'app\Models\User');})
->leftJoin('roles', 'roles.id', '=', 'model_has_roles.role_id')
->orderBy('roles.id')->get();
please note that if you want to order by multiple columns you could add 'orderBy' clause as much as you want:
->orderBy('roles.name', 'DESC')->orderby('teams.roles', 'ASC') //... ext
check my answer here:
https://stackoverflow.com/a/61194625/10573560

Laravel Eloquent maximize efficiency but keep elegant structure

Im working in a sensitive section of my app and i need to make sure to minimize the number of querys. I can easily do this with a multiple joins. The question is: is there a way to do this with beauty?
Elequent relationships are a good place to start but most of the time it requires multiple query.
The eager loading method used in this article looks alot better but still requires at least 2 querys and uses a whereIn statement instead of a join.
Article Example Of Eager Loading:
$users = User::with('posts')->get();
foreach($users as $user)
{
echo $user->posts->title;
}
Using Eager Loading, Laravel would actually be running the following
select * from users
select * from posts where user_id in (1, 2, 3, 4, 5, ...)
My current solution is to use laravel scopes in a way not intented.
public static function scopeUser($query) // join users table and user_ranks
{
return $query->join('users', 'users.id', '=', 'posts.user_id')
->join('user_ranks', 'users.rank_id', '=', 'user_ranks.id');
}
public static function scopeGroup($query,$group_id) // join feeds,group_feeds (pivot) and groups tables
{
return $query->join('feeds', 'feeds.id', '=', 'posts.feed_id')
->join('group_feed', 'feeds.id', '=', 'group_feed.feed_id')
->join('groups', 'groups.id', '=', 'group_feed.group_id')
->where("groups.id","=",$group_id);
}
The resulting query looks like this:
$posts = Post::take($limit)
->skip($offset)
->user() // scropeUser
->group($widget->group_id) // scropeGroup
->whereRaw('user_ranks.view_limit > users.widget_load_total')
->groupBy('users.id')
->orderBy('posts.widget_loads', 'ASC')
->select(
'posts.id AS p_id',
'posts.title AS p_title',
'posts.slug AS p_slug',
'posts.link AS p_link',
'posts.created_on AS p_create_on',
'posts.description AS p_description',
'posts.content AS p_content',
'users.id AS u_id',
'users.last_name AS u_last_name',
'users.first_name AS u_first_name',
'users.image AS u_image',
'users.slug AS u_slug',
'users.rank_id AS u_rank',
'user_ranks.name AS u_rank_name',
'user_ranks.view_limit AS u_view_limit'
)
->get();
Because of column name collisions i then need a huge select statement. This works and produces a single query, but its far from sexy!
Is there a better way to deal with big joined querys?
You could try to actually add the selects with aliases in the scope.
Note: This is totally untested
public static function scopeUser($query) // join users table and user_ranks
{
foreach(Schema::getColumnListing('users') as $column){
$query->addSelect('users.'.$related_column.' AS u_'.$column);
}
$query->addSelect('user_ranks.name AS u_rank_name')
->addSelect('user_ranks.view_limit AS u_view_limit');
$query->join('users', 'users.id', '=', 'posts.user_id')
->join('user_ranks', 'users.rank_id', '=', 'user_ranks.id');
return $query;
}
There is also no need to alias the post columns with a p_ prefix... But if you really want to, add another scope that does that and use addSelect()

Laravel: ordering a many-many relation

I have a many-many relation between Ingredient and Recipe, with a pivot table (ingredient_recipe).
I'd like to get ingredients ordered by how many recipes have them. Example, if I use salt in 2 recipes and meat in 3 recipes, I'll have meat before salt.
This is what I have. It works but it doesn't order correctly, even though the resulting query executed directly on my DB works as expected, so Laravel is doing something internally, I guess.
//Ingredient model
public function recipesCount()
{
return $this->belongsToMany('Recipe')->selectRaw('count(ingredient_recipe.recipe_id) as aggregate')->orderBy('aggregate', 'desc')->groupBy('ingredient_recipe.ingredient_id');
}
public function getRecipesCountAttribute()
{
if ( ! array_key_exists('recipesCount', $this->relations)) $this->load('recipesCount');
$related = $this->getRelation('recipesCount')->first();
return ($related) ? $related->aggregate : 0;
}
//controller
$ingredients = Ingredient::with('recipesCount')->whereHas('recipes', function($q)
{
$q->where('user_id', Auth::id());
})->take(5)->get();
//outputting the last query here and executing it on my db returns correctly ordered results.
How can I fix it?
In order to order by related table you need join. There's no way to achieve that with eager loading whatsoever.
Ingredient::with('recipesCount')
->join('ingredient_recipe as ir', 'ir.ingredient_id', '=', 'ingredients.id')
->join('recipes as r', function ($j) {
$j->on('r.id', '=', 'ir.recipe_id')
->where('r.user_id', '=', Auth::id());
})
->orderByRaw('count(r.id) desc')
->groupBy('ingredients.id')
->take(5)
->get(['ingredients.*']);
There's no need for whereHas anymore, for inner joins will do the job for you.

Complex joins in Eloquent

I have a table of itineraries. An itinerary belongs to a customer and has multiple days. A package is assigned to each of these days. I want to be able to produce a manifest showing which customers are allocated to a package and on which days.
I'm struggling with Eloquent, because you can't do queries beyond a one-to-Many relationship
What i want to do is this:
return $this->package->where('PackageID, $id)->itineraryDay->itinerary->customer->select('CustomerID', 'Date')
But can only really achieve it using the query builder:
return DB::connection($this->connection)
->table('t_package as PA')
->join('t_itinerary_day_map as IDM', 'IDM.PackageID', '=', 'PA.PackageID')
->join('t_itinerary_day as ID', 'IDM.ItineraryDayID', '=', 'ID.ItineraryDayID')
->join('t_itinerary as IT', 'IT.ItineraryID', '=', 'ID.ItineraryID')
->join('t_customer as CC', 'CC.ItineraryID', '=', 'IT.ItineraryID')
->where('PA.PackageID', $id)
->select('CC.CustomerID', 'ID.Date')
->distinct()
->get();
I really want to use Eloquent as I hate hardcoding table names and i've already created relationships for these models, but can't see any way around it
I believe you could do something like this to find customers that have a package with the given ID:
$packageId = 42;
$customers = $customer->whereHas('packages', function($q) use($packageId){
return $q->where('package_id', $packageId);
})->get();
How would that work for what you want?
I'll have to make few assumptions on your relationship but it seems doable.
If one ItineraryDay belongs to one Itinerary. And one Itinerary belongs to one Customer. And one ItineraryDay may have more than one Package.
$packageID = 111;
$itineraryDays = ItineraryDay::with('itinerary.customer')
->whereHas('package', function($q) use($packageID) {
$q->where('PackageID', $packageID);
})
->get();
foreach($itineraryDays as $itineraryDay) {
var_dump($itineraryDay);
var_dump($itineraryDay->itinerary->customer);
}
I'm not sure if i get your relationship method naming correct, but hopefully this works.

Categories