Laravel 5.4 - Eager Loading Child and Grandchild with Select Both - php

I am attempting to eager load a two deep relationship (child and grandchild) with selects on both descendants. However, when I add a addSelect() method to the grandchild it returns an empty array. What I have is the following:
$products = Category::with([
'products' => function($q){ $q->addSelect(['product_name', 'product_desc']);},
'products.productgroup' => function($q){ $q->addSelect(['price']);}
])->where('id', 1)->get();
This returns the Category and the product constraints, but the productgroup is returned as an empty array.
If I run the following:
$products = Category::with('products', 'products.productgroup')->where('id', 1)->get();
I get the expected return of all data including the productgroup data. It's only when I add the addSelect() method to the products or products.productgroup that it returns an empty array. Is there something i'm missing here?
I can't find any similar issues on stack or the laravel forums and i'm stumped.
EDIT: Including query from debugbar
The queries that came up in the debugbar are:
30.55ms
select * from `categories` where `id` = '1'
29.38ms
homestead
select `product_name`, `category_product`.`category_id` as`pivot_category_id`, `category_product`.`product_id` as `pivot_product_id` from `products` inner join `category_product` on `products`.`id` = `category_product`.`product_id` where `category_product`.`category_id` in ('1')
730μs
homestead
select `price` from `product_groups` where `product_groups`.`product_id` in ('')
I'm not 100% on how the query builder works under the hood with eager loading. It would appear that the product id isn't getting passed to the product_groups query when a addSelect() is present on either the product query or the productgroup query.

Ok so the answer to this is pretty simple. When selecting columns from the child and grandchild using addSelect(), I was not selecting the product.id or productgroup.product_id. The product.id and productgroup.product_id are obviously required in order to map the Grandchild to the Child node. It should be:
$products = Category::with(['products' => function($q){
$q->with(['productgroups' => function($g){
$g->addSelect(['id', 'price', 'product_id']);
}])->addSelect(['id', 'product_name', 'product_desc']);
}])->where('id', 1)->get();
Hope this helps anyone else who encounters a grandchild node of an eager load returning an empty array.
HT to Eddy The Dove
I used his nested formatting which reproduced the same issue, but was syntactically cleaner than my own way.

Try like this
$products = Category::with(['products' => function($q){
$q->with(['productgroup' => function($a) {
$a->addSelect(['price']);
}])->addSelect(['product_name', 'product_desc']);
})->where('id', 1)->get();

Try this if it works plz:
$product_ids = Category::find(1)->products()->pluck('id');
$prices = ProductGroup::whereIn('product_id', $product_ids)->pluck('price');

Related

Laravel orderBy with child relationship and parent at the same time

I have a list with messages. Its possible to reply to these messages (parent - child). I do not show child-messages in the list.
How can I always display the newest parent-message on top. Newest means that either the parent OR one of the childern has the newest timestamp.
Here is my eloquent query:
Message::withCount(['childMessages as latest_child_message' => function($query) {
$query->select(DB::raw('max(created_at)'));
}])
->orderByDesc('latest_child_message')
->orderByDesc('created_at')
->get();
Both orderBy should somehow be combined. Otherwise either the parent or the child sort will be prioritised.
In the context it's not possible to sort the collection after the DB-query.
edit 1:
Since "ee" is the latest response (child), the "bb" message should be at the bottom of the list.
edit 2:
The query will be used in a function returning a query
public static function getEloquentQuery(): Builder {
$query = parent::getEloquentQuery();
return $query->doTheMagicHere();
}
edit 3
This would be a working query.. but it's very slow
SELECT
id,
comment,
(SELECT MAX(cc.id) FROM comments cc WHERE cc.commentable_id = c.id) AS child_id
FROM
comments c
WHERE
commentable_type NOT LIKE '%Comment%'
ORDER BY CASE WHEN child_id IS NULL
THEN id
ELSE child_id
END DESC
;
In the withCount closure you must set conditions.
Use this:
Message::with('childMessages')->get()->sortByDesc(function ($parent, $key) {
$child = $parent->childMessages()->orderBy('created_at', 'desc')->first();
return $child ? $child->created_at : $parent->created_at;
});
the orderBy way you need is a bit complicated. it's better to use sortByDesc method and sort data on collection.
I hope this works.

How to limit my query to get only 5 products per category?

Alright, hello guys, I'm struggling for a while with this problem so I will need your help.
So here is the problem I have database with more than 6000 products and I wanted to get some specific categories and their sub categories and only the products associated with them.
Here is a sample array that I have made that stores the categories (the key is the chosen category ID and the array value is all of the sub categories ID's).
$products_categories = array(
3 => [
4, 5 , 6
],
10 => [
40, 30, 20
]
);
And that's my eloquent query to get all of the products with those categories.
$all_fetched_categories = array_merge(...$products_categories);
if (!empty($products_categories)) {
$products = Product::query();
$products = $products->with([
'media',
])
->active()
->top()
->whereHas('categories', function ($query) use ($all_fetched_categories) {
$query->whereIn('shop_category_id', $all_fetched_categories);
});
$products = $products->get();
}
So the question is, I will need only 5 products per main category. With the code that I provided I can't make logical limitation in the query.
You can't do it with database group by syntax.
Database group by returns single item from each group.
You need to use eloquent collection method groupBy().
By this method you can filter groups by whatever conditions you want.
Laravel eloquent groupBy() method doc
Fetch categories using whereIn and then add products eager loading with take(5) filter. Like this
$total = 5;
$categories = Categories::with(['products' => function($query) use ($total){
$query->latest()->take($total);
},'products.media'])
->whereIn('shop_category_id', $all_fetched_categories)
->get()
Assuming you have categories id in $all_fetched_categories

Laravel - query builder - left join polymorphic relationship, distinct only

So I'm using Vue 2.0 and Laravel 5.3 to create an application.
I've implemented my own sortable table with Vue, using the built-in pagination provided by Laravel.
Everything's working perfect - except, I'm trying to left join a "media" polymorphic table, so I can show an image in my table.
To enable sorting, I've had to use the query builder, as you can't sort on a relationship using Eloquent (AFAIK).
I define my relationships like:
$inventories = $inventories->leftjoin('users as inventory_user', 'inventories.user_id', '=', 'inventory_user.id');
$inventories = $inventories->leftjoin('categories as category', 'inventories.category_id', '=', 'category.id');
$inventories = $inventories->leftjoin('inventories as parent', 'inventories.parent_id', '=', 'parent.id');
Which work great. However, how exactly do I left join a polymorphic relationship, without repeating (duplicating) any of the rows?
I've got this:
$inventories = $inventories->leftJoin('media', function($q) {
$q->on('media.model_id', '=', 'inventories.id');
$q->where('media.model_type', '=', 'App\Models\Inventory');
});
Which does show media (if it has a relation in the polymorphic table). However, if for instance I have 5 images (media) for 1 particular inventory item, my query repeats the inventory for however media there are, which in my case it's repeated 5 times.
This is my select query:
$inventories = $inventories->select(DB::raw("
inventories.id as inventory_id,
inventories.name as inventory_name,
inventories.order_column as inventory_order_column,
category.id as category_id,
category.name as category_name,
parent.name as parent_name,
parent.id as parent_id,
inventories.updated_at as inventory_updated_at,
media.name
"));
I think I need to use a groupBy, but I'm not sure where I define it. If I do
$inventories = $inventories->groupBy('inventories.id');
I get
SQLSTATE[42000]: Syntax error or access violation: 1055 'test.inventories.name' isn't in GROUP BY...
Has anyone been in a similar situation or know where to put my groupBy to show 1/distinct inventory items?
EDIT:
I was able to fix by using:
$inventories = $inventories->leftJoin('media', function($q) {
$q->on('media.model_id', '=', 'inventories.id');
$q->where('media.model_type', '=', 'App\Models\Inventory');
$q->orderBy('media.order_column', 'asc');
$q->groupBy('model.model_id');
});
and then setting strict => false in my database.php.
As the error states this issue occurs when name column not being in GROUP BY clause.
To solve this change
'strict' => true,
In mysql configuration under connections in your config/database.php file to
'strict' => false,
Additionally I would highly encourage to use polymorphic relations over left join with eloquent.
I think you'll add it here:
$categories = $categories->select(DB::raw("
inventories.id as inventory_id,
inventories.name as inventory_name,
inventories.order_column as inventory_order_column,
category.id as category_id,
category.name as category_name,
parent.name as parent_name,
parent.id as parent_id,
inventories.updated_at as inventory_updated_at,
media.name
"))->groupBy('inventories.id');

Laravel Eloquent how to join on a query rather than a table?

I want to achieve this in Laravel:
SELECT * FROM products JOIN
(SELECT product_id, MIN(price) AS lowest FROM prices GROUP BY product_id) AS q1
ON products.id = q1.product_id
ORDER BY q1.lowest;
I wrote this, but clearly there is something wrong:
$products = new Product();
$products = $products->join(
Price::whereNotNull('price')->select('product_id', DB::raw('min(price) as lowest'))->groupBy('product_id'), 'products.id', '=', 'product_id'
)->orderBy('lowest')->get();
The error I got:
ErrorException in Grammar.php line 39:
Object of class Illuminate\Database\Eloquent\Builder could not be converted to string.
I'm currently using join(DB::raw('(SELECT product_id, MIN(price) AS lowest FROM prices WHERE price IS NOT NULL GROUP BY product_id) AS q1'), 'products.id', '=', 'q1.product_id') as a workaround. Just wondering how to do this in the Eloquent way? Thanks.
If you want to make a single query for efficiency reasons in this case Eloquent is not going to help you much, it is possible but is a hassle. For cases like this you have QueryBuilder.
DB::table('products')
->join(
DB::raw('(SELECT product_id, MIN(price) AS lowest FROM prices GROUP BY product_id) AS q1'),
'products.id', '=', 'q1.product_id'
)
->orderBy('q1.lowest')->get();
If you change get() for getSql() you get the following
select * from `products` inner join
(SELECT product_id, MIN(price) AS lowest FROM prices GROUP BY product_id) AS q1
on `products`.`id` = `q1`.`product_id` order by `q1`.`lowest` asc
Unfortunately as far as I know you can't use a subquery without DB::raw, nevertheless it is not insecure as long as you don't put user input in the query. Even in that case you can use it securely by using PDO.
As for Eloquent, your product model doesn't even have a price field (it probably has a prices() function returning a relationship object) so it makes no sense to get a Product model with a single price asociated to it.
Edit:
You can also eager load the relationship, i.e. (assuming you have the model relationship set as Trong Lam Phan's example)
$products = Product::with('prices')->get();
foreach ($products as $product)
{
$minPrice = $product->prices->min('price');
// Do something with $product and $minPrice
}
This will only run a single query but the min() operation is not done by the database.
In Eloquent, follow me, you need to think a little bit differently from normal mysql query. Use Model instead of complicated query.
First of all, you need to create Product and Price models with relationship between them:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
public function prices()
{
return $this->hasMany('\App\Price');
}
}
class Price extends Model
{
public function product()
{
return $this->belongsTo('\App\Product');
}
}
Then, you need to select all products:
$products = \App\Product::get();
foreach ($products as $product)
{
$minPrice = $product->prices->min('price');
echo 'Min price of product ' . $product->name . ' is: ' . $minPrice;
}
EDIT
If you have a problem with the performance, just get the product's id.
$products = \App\Product::get(['id']);
If you don't like this way of Eloquent, you may use Query Builder like that:
$result = \App\Price::join('products', 'prices.product_id', '=', 'products.id')
->select('product_id', 'name', \DB::raw("MIN(price) AS min_price"))
->groupBy('product_id')
->orderBy('min_price')
->get();
or you can do like that:
\App\Price::join('products', 'prices.product_id', '=', 'products.id')
->selectRaw("name, MIN(price) AS min_price")
->groupBy('product_id')
->orderBy('min_price')
->get()

Inner join single column using Eloquent

I am trying to get a single column of an inner joined model.
$items = Item::with('brand')->get();
This gives me the whole brand object as well, but I only want brand.brand_name
$items = Item::with('brand.brand_name')->get();
Didn´t work for me.
How can I achieve this?
This will get related models (another query) with just the column you want (an id, see below):
$items = Item::with(['brand' => function ($q) {
$q->select('id','brand_name'); // id is required always to match relations
// it it was hasMany/hasOne also parent_id would be required
}])->get();
// return collection of Item models and related Brand models.
// You can call $item->brand->brand_name on each model
On the other hand you can simply join what you need:
$items = Item::join('brands', 'brands.id', '=', 'items.brand_id')
->get(['items.*','brands.brand_name']);
// returns collection of Item models, each having $item->brand_name property added.
I'm guessing Item belongsTo Brand, table names are items and brands. If not, edit those values accordingly.
Try this:
$items = Item::with(array('brand'=>function($query){
$query->select('name');
}))->get();

Categories