Laravel: ordering a many-many relation - php

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.

Related

Laravel Spatie Query Builder - Wrong query when make join

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! :-)

Trying to related tables columns and sums with Laravel eloquent

Ok.
I have three tables
products
--product_id
--product_name
--product_type_id
--price15
--price23
--description
--bonus_points
--image
productTypes
--product_type_id
--product_type_name
productQuantities
--id
--product_id
--warehouse_id
--quantity
Products are placed in different warehouses so I have to keep tracks of its numbers
And has relationships are like this
class Product extends Model
{
public function productType() {
return $this->belongsTo('App\Models\ProductType','product_type_id','product_type_id');
}
public function productQuantities() {
return $this->hasMany('App\Models\ProductQuantity','product_id','product_id');
}
}
What I want to get is all columns from products and product type name from productType, sum of quantity from productQuantities, so I can perform search on those column values later on with where().
How can I get these columns with Eloquent?
I know I could get them with raw SQL commands but I need to do this way for compatibility reasons.
I tried this way before I ask the question.
But model relations just stopped working with no errors. Values just got emptied out from the other parts of the page.
$products = Product::selectRaw('products.*, productTypes.product_type_name, sum(product_quantities.quantity) as quantitySum')
->leftjoin('productTypes','products.product_type_id','=','productTypes.product_type_id')
->leftjoin('productQuantities','products.product_id','=','productQuantities.product_id')
->where('products.product_id','like','%'.$searchID.'%')
->where('product_name', 'like', '%'.$searchName.'%')
->where('product_type_name', 'like', '%'.$searchType.'%')
->where(function($q) use ($searchPrice) {
$q->where('price15','like','%'.$searchPrice.'%')
->orwhere('price23','like','%'.$searchPrice.'%');
})
->where('points', 'like', '%'.$searchPoints.'%')
->groupBy('products.product_id')
->orderByRaw($query)
->paginate($paginateBy);
Working version before this was simple.
Product::leftjoin('productTypes','products.product_type_id','=','productTypes.product_type_id')
->select('products.*','productTypes.product_type_name')
->where('products.product_id','like','%'.$searchID.'%')
->where('product_name', 'like', '%'.$searchName.'%')
->where('product_type_name', 'like', '%'.$searchType.'%')
->where(function($q) use ($searchPrice) {
$q->where('price15','like','%'.$searchPrice.'%')
->orwhere('price23','like','%'.$searchPrice.'%');
})
->where('points', 'like', '%'.$searchPoints.'%')
->orderByRaw($query)
->paginate($paginateBy);
And I thought any kind of join methods doesn't seem to be working well with Eloquent relationship? But older one has leftjoin method as well.
I have not tested this (and am assuming you want to group on product_type_name but you should be able to do something along the lines of:
$results = Product::with(['productType','productQuantities'])
->select(DB::raw('products.*,
productType.product_type_name,
sum(productQuantities.quantity) as "QuantitySum"'))
->groupBy('productType.product_type_name')
->get();
OR
$results = DB::table('products')
->join('productType', 'productType.product_type_id', '=', 'products.product_type_id')
->join('productQuantities', 'productQuantities.product_id', '=', 'products.product_id')
->select(DB::raw('products.*,
productType.product_type_name,
productType.product_type_name,
sum(productQuantities.quantity) as "QuantitySum"'))
->groupBy('productType.product_type_name')
->get();
Then you should be able to access the aggregated quantities using (in a loop if you wanted) $results->QuantitySum.
you can get it with eager loading and aggregating. For example, you need to query products has product type name like "new product" and quantity greater than 1000:
Product::with("productType")
->whereHas("productType", function ($query) {
$query->where("product_type_name", "like", "new product");
})
->withCount(["productQuantities as quantity_count" => function ($query) {
$query->selectRaw("sum(quantity)");
}])
->having("quantity_count", ">", 1000)
->get();
you can get through relationship
$product->productType->product_type_name
and attribute:
$product->quantity_count
$products = Product::withsum('productQuantities','quantity')
->leftjoin('product_types','products.product_type_id','=','product_types.product_type_id')
Gives me the result that I wanted. And didn't break the other parts.
But I'm still confused why with() and withSum() didn't work together.
Is it because products belongs to productTypes maybe

Laravel 5.6.8 Eloquent and many to many + many to one joins

I have many to many connect with between user - cityarea.
I have also area which connect cityarea (One cityarea can connect only one area).
I have this database structure:
users
id
username
password
cityareas
id
name
area_id
cityarea_user
id
cityarea_id
user_id
areas
id
name
Next I have Models
User
public function cityareas()
{
return $this->belongsToMany('App\Cityarea');
}
Cityarea
public function area()
{
return $this->belongsTo('App\Area');
}
public function users()
{
return $this->belongsToMany('\App\User');
}
Area
public function cityareas()
{
return $this->hasMany('App\Cityarea');
}
QUESTION:
How I can get all users where areas.name = "South" with Eloquent ?
Thanks!!
By using whereHas, you can do:
$users = User::whereHas('cityareas.area', function ($query) {
$query->where('name', 'South');
})->get();
Jeune Guerrier solution is perfect, but you can use with() method of eloquent If you also need cityarea collection along with users collection.
$users = User::with('cityareas')->whereHas('cityareas.area', function ($query) {
$query->where('name', 'South');
})->get();
This is exactly what the belongs to many relationships is built for.
You simply have to do, Cityarea::where('name', 'South')->first()->users;
If you want to do something further with the query, e.g. sort by users created at, you can do
Cityarea::where('name', 'South')->first()->users()->orderBy('creaated_at', desc')->get();
Note that if there is no such Cityarea with name 'South', the ->first() query above will return null and therefore will fail to fetch the users.
A more performant way to do it programmatically is to use the whereHas approach as discussed in the comments below.

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()

Categories