How can I build such a query using Laravel query builder? - mysql

There is a table of MySQL "Categories".
The Category model has a "hasMany" relationship that looks like this.
public function children(){
return $this->hasMany(self::class, 'parent_id');
}
On the category editing page, in order to assign the parent category to the current one (for this I need to fill in its parent_id field), I have to generate a Select tag that will contain all rows from the category table, except for the current one and all its child categories.
category_id | parent_id
------------------------
1 | NULL
2 | 1
3 | 2
4 | 3
5 | NULL
6 | 5
For example,
for category_id 1, should be selected lines with category_id [5, 6]
for category_id 2, should be selected lines with category_id [1, 5, 6]
for category_id 3, should be selected lines with category_id [1, 2, 5, 6]
for category_id 4, should be selected lines with category_id [1, 2, 3, 5, 6]
for category_id 5, should be selected lines with category_id [1, 2, 3, 4]
for category_id 6, should be selected lines with category_id [1, 2, 3, 4, 5]
If a category has parent_id is NULL, it means that it has no parent category.

Short answer: you need so select all records EXCEPT recursive records from the current category.
For the recursive part, you could be used this: https://github.com/staudenmeir/laravel-adjacency-list
Look at the descendantsAndSelf() relation it includes by default.
Next you need to build the EXCEPT query. The query builder does not include an except function, so you need to build one yourself.
For example I have this in one of my controllers:
I have an eloquent model called Thread, which is a model for holding message threads. People have different view rights on these threads so I need several queries to filter out the threads they can and cannot see.
I prebuild these separate queries using the element query builder, and some I UNION. Than there is one query which explicitly excludes threads. And than I combine the unioned and $excludeExplicit queries:
$query = Thread::query()
->fromRaw(
'(SELECT * FROM ((' . $unioned->toSql() . ') EXCEPT ' . $excludeExplicit->toSql() . ') AS threads) AS threads',
array_merge($unioned->getBindings(), $excludeExplicit->getBindings())
);
(See https://stackoverflow.com/a/64231210/9258054 )
The trick of this raw query is in the merging of the parameter bindings. Also, the result of this query are Eloquent models. Note that when using queries like UNION or EXCEPT you need to make sure that the SELECT statement of both return the same columns.

After digging around a bit in the code, I found a more elegant solution.
I am making a selection from the database using the "categories" relationship created in the "Article" model
public function categories(){
return $this->morphToMany(Category::class, 'categoryable');
}
In doing so, I only select those items that do not have a parent category.
But thanks to the "categories" relationship, all the children go to the "Article" collection, and the tree of items is built correctly, and I don't have to do complex queries with "Join".
public function topMenu(){
View::composer('layouts.header', function ($view){
$view->with('categories', Category::whereNull('parent_id')->where('published', 1)->get());
});
}

Related

Why items repeat in Prisma pagination with orderBy?

The data looks as follows:
13 users with id's 2-13. User 2 got 2 likes, user 10 got 2 likes, user 3 got 1 like. The rest didn't get any likes.
Prisma query looks like this:
return this.prisma.user.findMany({
skip: Number(page) * Number(size),
take: Number(size),
orderBy: { likesReceived: { _count: "desc" }
});
When I send a query to the database, ordering by likesReceived I get these responses:
page
size
items id's
0
5
2, 10, 3, 4, 14
1
5
6, 7, 8, 9, 11
2
5
12, 13, 14
User 14 appears twice, and user 5 is missing. Why?
Additional sorting by id fixes the problem:
return this.prisma.user.findMany({
skip: Number(page) * Number(size),
take: Number(size),
orderBy: [{ likesReceived: { _count: "desc" } }, { id: "asc" }],
});
Results:
page
size
items id's
0
5
2, 10, 3, 4, 5
1
5
6, 7, 8, 9, 11
2
5
12, 13, 14
When is specifying a second parameter in orderBy with pagination necessary for it to work well?
I agree with the provided answer, just posting here what I replied on the Prisma repo issue directly:
What I suspect is happening here is that in the first case where you order only by the count, the count values are not unique (if I got your description correctly a lot of them have count 0). In this case I don't think the order within the values with the same count is stable between different requests at the database level. This is normal in the SQL world. The solution is to add a tiebreaker, a second field to order by that ideally is unique. This you did in the second request and then got a stable ordering.
So I'd say this is not a bug but expected behaviour from the database and therefore Prisma. The fix you already found, just add a second unique field to break ties or use that one as cursor directly.
I had a similar issue with Laravel using sqlserver.
Laravel was doing a different query for the first page than the subsequent pages. For page 1 they used...
SELECT TOP 100 * FROM users
while for subsequent pages they used row_number(), something like...
SELECT * FROM (
SELECT
ROW_NUMBER() row_num,
*
FROM
users
) u
WHERE
row_num > 100 AND row_num <=201;
Sqlserver doesn't do a default order by Default row order in SELECT query - SQL Server 2008 vs SQL 2012, rather each time it will choose the most optimized way. Therefore on the page 1 query using TOP it chose one way to order and on page 2 with row_number() it chose a different way to order. Thereby returning duplicate results in page 2 that were already in page 1. This was true even though I had many other order bys.
Mysql also seems not to have a default order by SQL: What is the default Order By of queries?.
I don't know if Prisma does the same thing with mysql. Printing out the queries may shed light on if different queries are used for different pages.
Either way if you're using pagination it may make sense, to do as you mentioned and to always use id as a final order by. Like this even if your other intended order bys allow the same record to be on multiple pages the final order by id will ensure that doesn't occur, since now you're forcing it to order by ids instead of choosing a more optimal approach that doesn't order by ids.
In your case since user 14 has 0 likes it can be on any page after 2, 10 and 3 and still satisfy your likesReceived orderBy. But with the id order by then it'll always be on page 2, since page 1 will now have 4 and 6 as the last records, instead of 14, due to the 2nd orderBy of id.

Multi Ordering 4 SQL columns with a single query

Environment: MySQL 5.6
SqlTable name = CategoryTable
Sql Columns
CATEGORY_ID (INT)
CATEGORY_NAME (VARCHAR)
LEVEL (INT)
MOTHER_CATEGORY (INT)
I've tried with
SELECT
CATEGORY_ID, CATEGORY_NAME , LEVEL , MOTHER_CATEGORY
FROM
CategoryTable
But I don't know how to use the ORDER BY in order to get that result.
So the first line here are the columns, and from the second lines, there start the table content:
CATEGORY_ID CATEGORY_NAME LEVEL MOTHER_CATEGORY
1 MainCategory 0 0
2 -SubCategory1 1 1
3 --SubCategory2 2 2
4 ---SubCategory3 3 3
5 2Nd_Main_Category 0 0
6 -SubCategory1 1 5
7 --SubCategory2 2 6
8 ---SubCategory3 3 7
is there a way to achieve something like this with a mysql query?
You aren't very clear in what you are trying to achieve. I'll take a guess that you want to order using a multi-level parent child structure. there are some very complicated ways of handling such a feat within mysql 5.6, a DB that's not really ideal for such a structure, but I have come up with something simple myself that I use in my own apps. you create a special ordering field that creates a path of zero filled ids for each record.
ordering_path_field
/
/0000000001/
/
/0000000001/0000000002
/0000000003
/0000000003/0000000005
/0000000003/0000000005/0000000006
etc
so each record contains a path of each parent up to the root, using zero filled ids. then you can just sort by this field to get them in proper order. the drawbacks being that you'll have to set a max number of levels allowed, so that the ordering fields doesn't overflow, and also, moving a record to a new parent if ever needed would be a big pain.

Updating a table based on contents of the same table

I have a table:
id, number, name, display_name
0001, 1, Category 1, null
0001-0002, 2, Category 2, null
0001-0002-0003, 3, Category 3, null
The id is the full path to the category, the number is just the final category number.
I'd like display_name updated to include the full names from all categories in the path, so they'd end up as
0001, 1, Category 1, Category 1
0001-0002, 2, Category 2, Category 1 > Category 2
0001-0002-0003, 3, Category 3, Category 1 > Category 2 > Category 3
I know I can generate these on the fly by lookups to the number column, but this table doesn't need to change that often but it has a lot of lookups -- it seems wasteful not to just calculate and store the data once. I can do this in php but it's slow and I assume there's a better way to do it? Or perhaps I'm just going about this in completely the wrong way. I realise there's plenty of redundancy in the table... I'm happy for any input.
I got as far as
update categories set display=(select name from (select name from categories where number=1) t) where number=1
but that obviously just copies the name to the display name.

How to explode in MySQL and use it in the WHERE clause of the query - MySQL

I have a database table as below.
Promotion_Table
id(INT), promotion_name(VARCHAR),......, bungalow_ids(VARCHAR)
We can add a promotion for a bungalow(23). So a row is added with the bungalow id as below.
1, My Promotion, ........, 23
But if I single a promotion is added for a multiple bungalows(23,42) all ids are saved in the bungalow_ids column as below.
2, My Promotion 2, ........, 23 | 42
If a user search for promotion which are for specific bungalow(23) All promotions for the bungalow should be shown in the result.
I have a query as below.
SELECT * FROM Promotion_Table WHERE bungalow_ids = '23'
It only gets 1 rows. But actually 2nd row should be shown too since there is a offer. I can nt use LIKE since it gets wrong records.
Given that I have already referred below links but I have no idea how to use them in the query.
Can you split/explode a field in a MySQL query?
Equivalent of explode() to work with strings in MySQL
How can I fix this? How can I explode the column data and use it in the query ?
Use , to separate the string and try this query
select * from promotion_table where FIND_IN_SET("23",bungalow_ids)
http://sqlfiddle.com/#!2/7bbcb/1
The previous Answer is the right decision but if you insist in your model.
Probably what you want to do is:
SELECT *
FROM Promotion_Table
WHERE bungalow_ids = '23'
OR bungalow_ids LIKE '23,*'
OR bungalow_ids LIKE '*,23'
OR bungalow_ids LIKE '*,23,*'
this assuming the numbers are separated by ",".
But this is the wrong way, make the changes to the DB as stated in the previous answer.
you need to reformat your DB schema.
you need to construct 2 tables one for promotions and one for bangalows.
like below:
promotions: Promotion_id(int), Promotion_desc
bangalows: Bangalow_id(int), Promotion_id(int)
tables example:
promotion :
1 myPromotion
2 secondPromotion
bangalows:
1 1
2 2
3 1
4 1
once you create above two tables, the following query will work and returns 1,3,4:
SELECT Bangalow_id FROM Promotion_Table WHERE bungalow_id = '1'

Get All Parents of Hierarchical Category in MySQL

I have a table that looks like the following
cat_id | name | parent_id | Level
-------------------------------------------------
1 cat1 0 1
2 subcat1 1 2
3 subcat1-subcat 2 3
I am wondering what is the most efficient way to get the parent categories of cat_id 3, so my result set will look like this
cat_id | name | parent_id | Level
--------------------------------------------------
1 cat1 0 1
2 subcat1 1 2
You have to do multiple queries. One for each level up you want to go (or write it as a stored procedure).
The most efficient way is not to use what's called the "adjacency list model" (what you are using) and instead switch to "nested sets".
Googling for "nested set" will give you lots of info. It will take you some time to get used to it though, and to write the code for handling it.
I was having the same issue, but unfortunately there is no way to do this in single query. so you have to either write a function or a stored procedure which can get all parent categories.
algo will be as follows
**childs** = 3
Loop (not done)
Get immediate parents of **childs**,
save(concat) them in a *return* variable
update **childs** = immediate parents
REPEAT
return will contain all parents(flat) of given category
1) FUNCTION : in case of function you will return a string value as function can not return more than 1 values so your result will be some thing like "1,2". but again this will not full fill our purpose as we have to use this result in a query
SELECT * FROM table_name where id IN ("1,2") ORDER BY LEVEL
so instead we will return our result as a regular expression ;) and that is "^(1|2)$"
SELECT * FROM tbl_categories WHERE cat_id REGEXP "^(1|2)$" ORDER BY level;
2) STORED PROCEDURE: in case of stored procedure we can prepare a statement dynamically and upon executing that query we will have our required result.
for more detail on stored procedure pleas have a look on following tutorial.
Get all nested parents of a category – MySQL