I have 4 tables, items, listings, catitem_item, and item_listing.
items and listing is many to many relationship.
items and catitems is also many to many relationship.
catitems contains list of item's categories.
listings is like location where items located.
example listing shop A can have item chair and item chair have multiple catitem categories.
My goal is to get items which under list of categories such as category 1 AND 2 AND 3 ($cats) and with listing information where this item located.
So if there are 6 listings for chair, it will return 6 chair results.
This is the query I have so far.
$items = DB::table('items')
->join('catitem_item', 'catitem_item.item_id', '=', 'items.id')
->join('item_listing', 'item_listing.item_id', '=', 'items.id')
->join('listings', 'item_listing.listing_id', '=', 'listings.id')
->whereIn('catitem_item.catitem_id', $cats)
//->groupBy('items.id')
//->having(DB::raw('count(*)'), '=', count($cats))
->select('items.id', 'items.name', 'items.price', 'items.slug', 'item_listing.listing_id', 'listings.name as listing_name', 'listings.slug as listing_slug')
->get();
Note that the way you are trying to do it, you might get multiple rows per item (once per related listing). A better way would be to have an array of listings per item.
If you use eloquent models and you have setup the relations correctly, you could try the following:
$cats = [1, 2, 3];
$query = Item::with('listings');
foreach ($cats as $cat) {
$query->whereHas('catitems', function($q) use($cat) {
$q->where('id', $cat);
});
}
$items = $query->get();
Now every item shoud have a listings property. For example for the first item you can access the listings the following way:
$item1 = $items[0];
$listings1 = $item1->listings;
Note that whereHas() will probably create a correlated EXISTS subquery for every entry in the $cats array. If that is to slow, you can use a JOIN query like:
$items = Item::with('listings')
->join('catitem_item', 'catitem_item.item_id', '=', 'items.id')
->whereIn('catitem_item.catitem_id', $cats)
->groupBy('items.id')
->having(DB::raw('count(*)'), '=', count($cats))
->select('items.*')
->get();
If you don't use eloquent, you can also do the "eager loading" yourself.
$items = DB::table('items')
->join('catitem_item', 'catitem_item.item_id', '=', 'items.id')
->whereIn('catitem_item.catitem_id', $cats)
->groupBy('items.id')
->having(DB::raw('count(*)'), '=', count($cats))
->select('items.*')
->get()
->keyBy('id');
foreach ($items as $item) {
$item->listings = [];
}
$itemIds = $items->pluck('id');
$listings = DB::table('listings')
->join('item_listing', 'item_listing.listing_id', '=', 'listings.id')
->whereIn('item_listing.item_id', $itemIds)
->groupBy('listings.id')
->select('listings.*', DB::raw('group_concat(item_listing.item_id) as item_ids'))
->get();
foreach ($listings as $listing) {
$itemIds = explode(',', $listing->item_ids);
foreach ($itemIds as $itemId) {
$items[$itemId]->listings[] = $listing;
}
$listing->forget('item_ids');
}
Related
I'm taking data from my Animals table, which has a relationship with the Categories table. My problem is that I need to count how many products per category are registered.
Doing a search here on Stackoverflow, I started using the code below that is returning the quantity of animals per category ([Animal, quantity]).
$data = DB::table('animals')
->select(
DB::raw('category_id as category'),
DB::raw('count(*) as number'))
->groupBy('category')
->get();
$array[] = ['Name', 'Quantity'];
foreach($data as $key => $value)
{
$array[++$key] = [$value->category, $value->number];
}
$cat = json_encode($array);
dd($cat);
Using "dd", I see that the data below is correct, but the category_id is coming, I am not sure how to get this id and put the category name for that id.
"[["Category","Quantity"],[1,10],[2,14],[3,30],[4,26],[5,1]]"
Example: [2,14] this refers to category_id 2 which has the name: mammal. So I would have 14 animals registered in the mammal category.
I would like the result to be like this:
"[["Category","Quantity"],[birds,10],[mammals,14],[reptiles,30],[amphibians ,26],[fish,1]]"
How can I handle this id related to the category name?
Join your category table and get name from there, I suppose it should look like:
$data = DB::table('animals')
->join('category', 'animals.category_id', '=', 'category.id')
->select(
DB::raw('category.id as category_id'),
DB::raw('category.name as category_name'),
DB::raw('count(*) as number'))
->groupBy('category')
->get();
More about joins
You can use the method "withCount" from the model, it helps you to count the number of results from a relationship
$posts = App\Post::withCount('comments')->get();
foreach ($posts as $post) {
echo $post->comments_count;
}
See the documentation https://laravel.com/docs/7.x/eloquent-relationships#counting-related-models
I have a Model with manyToMany relationship:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
public function categories()
{
return $this->belongsToMany(Category::class, 'product_category', 'product_id', 'category_id');
}
}
So 1 Product can have multiple Categories.
I'm trying to query (fetch) all Products that have exact match of categories:
Example:
$categories = ['category1', 'category2'];
Product::whereHas('categories', function($q) use ($categories){
foreach ($categories as $categoryName) {
$q->where('name', $categoryName);
}
//or $q->whereIn('name', $categories);
})->get();
Also tried:
$categories = ['category1', 'category2'];
Product::whereHas('categories', function($q) use ($categories){
$q->whereIn('name', $categories);
}, '=', count($categories))->get();
Assume my Product has only category1 attached, the query should return this product.
If Product has both categories, but my array contains only category1, then this Product should be ignored.
So I'm trying to achieve: Fetch only products with specific categories. It is doable with Eloquent or DB builder?
Pseudo Code
$allow = for each Product->categories{
category Exist in $categories
}
Based on your requirement your MySQL query would be as follow:
Use LEFT JOIN to get total numbers of mapped categories for a product.
Use GROUP BY on a product to compare its total mapped categories vs matching categories based on user input.
Above comparison can be done using HAVING clause, where total mapped categories should be equal to the count of categories the user has provided. Same matching categories based on user input should also match the exact count of categories which the user has provided.
SELECT p.id
FROM products p
LEFT JOIN product_category pc ON p.id = pc.product_id
LEFT JOIN categories c ON c.id = pc.category_id AND c.name IN ('category1', 'category2')
GROUP BY p.id
HAVING COUNT(0) = 2 -- To match that product should have only two categories
AND SUM(IF(c.name IN ('category1', 'category2'), 1, 0)) = 2; -- To match that product have only those two categories which user has provided.
Same can be achieved by query builder in the following manner:
$arrCategory = ['category1', 'category2'];
$strCategory = "'" . implode("','", $arrCategory) . "'";
$intLength = count($arrCategory);
$products = Product::leftJOin('product_category', 'products.id', '=', 'product_category.product_id')
->leftJoin('categories', function ($join) use ($arrCategory) {
$join->on('categories.id', '=', 'product_category.category_id')
->whereIn('categories.name', $arrCategory);
})
->groupBy('products.id')
->havingRaw("COUNT(0) = $intLength AND SUM(IF(categories.name IN ($strCategory), 1, 0)) = $intLength")
->select(['products.id'])
->get();
dd($products);
Note: If you have only_full_group_by mode enabled in MySQL, then include columns of products table in both group by and select clause which you want to fetch.
Try using this without wherehas
Product::with(array('categories' => function($q) use ($categories){
$query->whereIn('name', $categories);
}))->get();
Thank you for your answers I solved this with: whereExists of sub select fromSub and array_agg, operator <# (is contained by):
$categories = ['category1', 'category2'];
Product::whereExists(function ($q) use ($categories){
$q->fromSub(function ($query) {
$query->selectRaw("array_agg(categories.name) as categoriesArr")
->from('categories')
->join('product_category', 'categories.id', '=', 'product_category.category_id')
->whereRaw('products.id = product_category.product_id');
}, 'foo')->whereRaw("categoriesArr <# string_to_array(?, ',')::varchar[]", [
implode(",", $categories)
]);
})->get();
I am trying to convert the following database query to use Eloquent:
$orderitems = DB::table('order_items')
->join('orders', 'orders.id', '=', 'order_items.order_id')
->where('orders.status', 1)
->where('order_items.sku', $sku)
->select('order_items.*')
->get();
I've tried the following:
$orderitems = Item::where('sku', $sku)
->with(['order' => function ($query) {
$query->where('status', 0);
}])
->get();
but it seems to be bringing back more results than it should. I can see from using DB::getQueryLog that the order_items query is not joining with the orders table:
select * from `order_items` where `stock_code` = ? and `order_items`.`deleted_at` is null
Do I need to create a new relationship between orderitems and order and add the additional where into that?
Relationships:
Order hasMany Items
Item hasOne Order
Solution was to use Query Scopes https://laravel.com/docs/5.7/eloquent#query-scopes
In the controller:
$orderitems = Item::stock($sku)->paginate(10);
In the orderitems model:
public function scopeStock($query, $sku)
{
return $query->join('orders', function ($join) {
$join->on('orders.id', '=', 'order_items.order_id');
$join->where('orders.status', '=', 1);
})
->select('order_items.*', 'orders.order_date')
->where('order_items.sku', $sku);
}
I want to get my products info from AppServiceProvider
Logic
Category -> Subcategory -> Product
now I need to get products base on category id
note: you might think it's strange but it's not really, just imagine you want show products in category page from all subcategories of that category. Then you'll need such thing like i do. getting products base on category_id while you only save subcategory_id in products table.
this is my loop in AppServiceProdiver:
View::composer('welcome', function ($view) {
$categories = Category::join('admin_designs', 'admin_designs.category_id', '=', 'categories.id')->get();
foreach($categories as $category){
$subcategory = Subcategory::join('categories', 'categories.id', '=', 'subcategories.category_id')->first();
$designs = Product::where('subcategory_id', $subcategory)->first();
}
$view->with('categories', $categories);
});
result of that loop is:
{"id":2,"title":null,"slug":"laptop","image":"category-1516430091.jpg","imageAlt":null,"status_id":1,"meta_tags":"laptop,tags","meta_description":"laptop description","created_at":"2018-02-01 11:41:30","updated_at":"2018-02-01 11:41:30","category_id":1},
PS: have no idea what is that! is not product/ is not complete
category less title!... :|
anyone can help with fixing that query?
SOLVED
View::composer('welcome', function ($view) {
$category = DB::table('admin_designs')
->join('categories', 'categories.id', '=', 'admin_designs.category_id')
->join('subcategories', 'subcategories.category_id', '=', 'categories.id')
->join('products', 'products.subcategory_id', '=', 'subcategories.id')
->get();
$view->with('category', $category);
});
Hope it helps.
I want to return all items with have all categories.
$items = DB::table('items')
->join('catitem_item', 'catitem_item.item_id', '=', 'items.id')
->whereIn('catitem_item.catitem_id', $cats)->paginate(10);
This query what I have now return items that have category 1 or 2 or 3.
What I need is return items that have category 1 and 2 and 3.
How can I achieve this?
Try this one:
$items = DB::table('items')
->join('catitem_item', 'catitem_item.item_id', '=', 'items.id')
->whereIn('catitem_item.catitem_id', $cats)
->groupBy('items.id')
->having(DB::raw('count(*)'), '=', count($cats))
->select('items.*')
->paginate(10);
With HAVING count(*) = 3 the query will only return items that have all listed categories.
Try like this:
Where $cats= [1,2,3];
$items = DB::table('items')
->join('catitem_item', 'catitem_item.item_id', '=', 'items.id')
->whereIn('catitem_item.catitem_id', $cats)
->paginate(10);
its return items for category 1 and 2 and 3.that means its use for AND opration.