Laravel belongsToMany where doesn't have one of - php

I have two tables: categories and videos, I then have a pivot table for these as it's a belongsToMany relationship.
What I'm trying to do is get all of the videos where there isn't a single instance of the video being in one of many categories.
e.g.
Video 1 is in category 1, 2 and 3.
Video 2 is in category 1 and 3.
Video 3 is in category 1.
I want to get the video which is NOT in category 2 or 3, meaning this will return Video 3.
What I've tried so far, which doesn't give the intended result, this is because another row is still found for Video 1 and 2, as they are in Category 1:
Video::whereHas('categories', function($query) {
$query->whereNotIn('category_id', [2,3]);
})->take(25)->get();
The query populated from this is:
select * from `videos` where exists (select * from `categories` inner join
`category_video` on `categories`.`id` = `category_video`.`category_id` where
`videos`.`id` = `category_video`.`video_id` and `category_id` != ? and
`category_id` != ? and `categories`.`deleted_at` is null) and `videos`.`deleted_at`
is null order by `created_at` desc limit 25

You can use Eloquent's whereDoesntHave() constraint to get what you need:
// get all Videos that don't belong to category 2 and 3
Video::whereDoesntHave('categories', function($query) {
$query->whereIn('id', [2, 3]);
})->get();

Related

Laravel Eloquent : Select random rows on relation based

I want to pick questions randomly but category dependent. For example, if the test is given with 10 questions and the total category is 5, the test flow should take 2 questions randomly from each category. Is there a way to select it through random and eloquent relations?
and the question table
+-------+-------+-------+-------+
| id | category_id |.......|
+-------+-------+-------+-------+
already I am using random eloquent but the probability of getting questions from each category is low
public getRandomQuestions($limit)
{
$this->inRandomOrder()->limit($limit)->get()
}
and I'm clueless when it's coming to relations.
You can also use the inRandomOrder and groupBy method together to select random questions from each category.
$questions = Question::with('category')->inRandomOrder()->groupBy('category_id')->limit(2)->get();
This will give you 2 random questions from each category.
You can also use subquery to select questions with certain number of random questions per category
$questions = Question::with('category')
->whereIn('category_id', function($query) use ($limit) {
$query->select('category_id')
->from('questions')
->groupBy('category_id')
->inRandomOrder()
->limit($limit)
})->get();
the query to get 1 random question for each category:
SELECT *
FROM
(SELECT *,
#position := IF(#current_cate=category_id, #position + 1, 1) AS POSITION,
#current_cate := category_id
FROM
(SELECT q.*
FROM category c
INNER JOIN question q ON c.id = q.category_id
ORDER BY RAND()) temp
ORDER BY category_id) temp1
WHERE POSITION <= 2
ORDER BY category_id;
explanation:
since you want the question to be take randomly we need order by rand(), note: inRandomOrder also uses order by rand() under the hood
to be able to get 2 questions for each category, we need a variable (#position) to mark the order of question
laravel implementation:
public getRandomQuestions($limit)
{
$questions = DB::select("SELECT *
FROM
(SELECT *,
#position := IF(#current_cate=category_id, #position + 1, 1) AS POSITION,
#current_cate := category_id
FROM
(SELECT q.*
FROM category c
INNER JOIN question q ON c.id = q.category_id
ORDER BY RAND()) temp
ORDER BY category_id) temp1
WHERE POSITION <= 2
ORDER BY category_id");
return Question::hydrate($questions->toArray());
}
If you're using PHP >= 7.2 the use shuffle()
public function getRandomQuestions($limit) {
return Question::limit($limit)->groupBy('category_id')->get()->shuffle();
}
or else
public function getRandomQuestions($limit) {
return Question::inRandomOrder()->limit($limit)->groupBy('category_id')->get();
}
the trick is you need to use groupBy() clause for this

laravel select 5 random rows from the 20 most recent rows ( without mapping after load & do with one query )

how I can do this
Ex: select 5 random rows from the 20 most recent row
I need to do this with eloquent and get data with a query from DB not with mapping data
I found this query but I can't convert this to laravel eloquent …
Query :
select tbl1.* from (select *from DemoTable ORDER BY ShippingDate DESC LIMIT 20 ) as tbl1
-> ORDER BY RAND() LIMIT 5;
you can use this code. Change Models and columns for your models and columns. I wrote a user for the test
return User::query()
->whereIn('id', function($query)
{
$query->from('users')
->selectRaw('id')
->orderByDesc('created_at')->limit(20);
})->inRandomOrder()->limit(5)->get();
if you get toSql() you see
select * from `users` where `id` in (select id from `users` order by `created_at` desc limit 20) limit 5

how to batch update the unique column based on different condition in a single query mysql

======= products table ======
CREATE TABLE `products` (
`id` BigInt( 11 ) UNSIGNED AUTO_INCREMENT NOT NULL,
`item` VarChar( 50 ) CHARACTER SET utf8 COLLATE utf8_general_ci NULL,
`category` VarChar( 20 ) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
PRIMARY KEY ( `id` ),
CONSTRAINT `unique_item` UNIQUE( `item` ) )
CHARACTER SET = utf8
COLLATE = utf8_general_ci
ENGINE = InnoDB
AUTO_INCREMENT = 1;
====== existing data in products table ======
INSERT INTO `products` ( `category`)
VALUES ( '2' ),( '2' ),( '3' ),( '5' );
====== update batch query ======
UPDATE products
SET item = CASE
WHEN category = 2
THEN 222
WHEN category = 2
THEN 211
WHEN category = 3
THEN 333
WHEN category = 5
THEN 555
END
WHERE category IN (2, 3, 5) AND item IS NULL
I have 3 columns (id, category, item) in products table.
id is primary key and item is unique key.
I want to use a single query to update item at once based on different category. The above update batch query is only working for category 3 and 5. It does not work for category 2.
The error message:
Error( 1062 ) 23000: "Duplicate entry '222' for key 'unique_item'"
I can use SELECT and UNION to get the list of id and put the id in CASE WHEN for update. But I would like to reduce one step. Is it possible to update all items in one query?
Thanks in advance!
===== EDITED =====
Current data in the product table
Expected result after run update query
It is possible to create a single query and update all data in one step.
Generate the folowing subquery in PHP from your update data:
select 0 as position, 0 as category, 0 as item from dual having item > 0
union all select 1, 2, 222
union all select 2, 2, 221
union all select 1, 3, 333
union all select 1, 5, 555
The first line here is only to define the column names and will be removed due to the impossible HAVING condition, so you can easily add the data rows in a loop without repeating the column names.
Note that you also need to define the position, which must be sequential for every category.
Now you can join it with your products table, but first you need to determine the position of a row within the category. Since your server probably doesn't support window functions (ROW_NUMBER()), you will need another way. One is to count all products from the same category with a smaller id. So you would need a subquery like
select count(*)
from products p2
where p2.category = p1.category
and p2.item is null
and p2.id <= p1.id
The final update query would be:
update products p
join (
select p1.id, ud.item
from (
select 0 as position, 0 as category, 0 as item from dual having item > 0
union all select 1, 2, 222
union all select 2, 2, 221
union all select 1, 3, 333
union all select 1, 5, 555
) as ud -- update data
join products p1
on p1.category = ud.category
and ud.position = (
select count(*)
from products p2
where p2.category = p1.category
and p2.item is null
and p2.id <= p1.id
)
where p1.item is null
) sub using (id)
set p.item = sub.item;
However I suggest first to try the simple way and execute one statement per row. It could be something like:
$stmt = $db->prepare("
update products
set item = ?
where category = ?
and item is null
order by id asc
limit 1
");
$db->beginTransaction();
foreach ($updateData as $category => $items) {
foreach($items as $item) {
$stmt->execute([$item, $category]);
if ($db->rowCount() == 0) {
break; // no more rows to update for this category
}
}
}
$db->commit();
I think the performance should be OK for like up to 1000 rows given an index on (category, item). Note that the single query solution might also be slow due to the expensive subquery to determine the position.
You cannot do what you want, because category is not unique in the table.
You can remove the current rows and insert the values that you really want:
delete p from products p
where category in (2, 3, 5) and itemid is null;
insert into product (itemid, category)
values ('222', '2' ), ('211', '2' ), ('333', '3' ), ('555', '5' );
MySQL can't make sense of your CASE expression.
You wrote
CASE
WHEN category = 2 THEN 222
WHEN category = 2 THEN 211
What do you want to happen when category is 2? You have provided two alternatives, and MySQL can't guess which one you want. That's your duplicate entry.
The error message is admittedly a little confusing.

laravel orm : where condition on table -> related table -> related table

so here is my database for a book store
books : id ,title
category : id, title
book_category : id , book_id, category_id
book_stock : id , book_id , quantity , price
considering all the relations are defined in the model , i can query book_stock it goes something like this
Stock::with('Book')->get();
but what if i want to get stock of a book in the category = 1
i can use use condition on book
Stock::with('Book' , function($q){
$q->where(['title'=>'abc']);
})->get();
but how can i filter related table of book ?
basically i want to get book_id from book_category where category_id = 1 and then use those ids to filter my books finally get stock
ps : i don't want to use query builder
This will return you all books belonging to category=1 with their stock information:
$categoryId = 1;
$books = Book::with('stock')->whereHas('category', function($query) use ($categoryId) {
return $query->where('id', $categoryId);
})->get();
You can do:
Stock::with('Book.stock', 'Book.category')->get();
You can access any number of nested relations within a with statement.
Related question:
Laravel nested relationships
Armin Sam's answer should also be a viable option.

How to do Subquery for same Table in Laravel Eloquent

I have structured a categories table to store Categories as well as Sub Categories.
My table Structure is:
catID (int)
catType (varchar 4)
parentID (int)
catName (varchar 50)
sOrder (int)
How to do following subquery with Eloquent.
SELECT c.*,
(
SELECT COUNT(*) FROM categories AS sc WHERE sc.parentID = c.catID AND sc.catType = "Sub"
) AS totalSubCategories
FROM categories AS c
WHERE c.catType = 'Root'
I have tried following but it wont work:
$rows = CategoryModel::from('categories as c')
->where('c.catType', '=', 'Root')
->paginate(20)
->select(DB::raw('c.*, (SELECT COUNT(*) FROM categories as sc WHERE sc.parentID = c.catID AND sc.catType = "Sub") AS totalCategories'));
I am getting following Error
ErrorException (E_UNKNOWN)
call_user_func_array() expects parameter 1 to be a valid callback, class 'Illuminate\Support\Collection' does not have a method 'select'
You can add an Eloquent relationship on the same table, and filter it to the right catType:
public function children() {
return $this->hasMany('Model', 'parentID')->where('catType', 'Sub');
}
This makes the item's children available as $item->children. You can get a count like so:
$count = $item->children()->count();

Categories