Adapt query to return category hierarchy by keyword

和自甴很熟 提交于 2019-12-02 23:27:45

问题


I asked this question previously, then someone suggested that it was a duplicate of another previously answered question. However, I could not adapt that solution to what I need despite 3 hours of trying.

So, my new question is how to adapt that solution to my own needs.

A simplified version of my category/subcategory database schema looks like this:

tblAllCategories

record_id  title                    level   parent_cat_id   parent_id   keywords
-------------------------------------------------------------------------------------------
1          Antiques & Collectables  0       NULL              NULL          junk
2          Art                      0       NULL              NULL          
25         Furniture                1       1                 1             
59         Office Furniture         2       1                 25            retro,shabby chic
101        Chairs                   3       1                 59            

Notes:

  • Level 0 = top-level category, level 1 = second level, etc
  • parent_cat_id is the top-level category (i.e. having level 0)
  • parent_id refers to the level immediately above the relevant level

I added the keyword column to assist keyword searches so that items in certain relevant categories would be returned if the user entered a keyword but did not select a category to drill down into.

So, at the front end, after the user enters keyword, e.g., "Retro", I need to return not only the category that has the term "retro" in its keyword column, but also all higher level categories. So, according to the schema above, a search on "retro" would return category 59 along with its super-categories - 25 and 1.

The query should be sorted by level, such that the front end search results would look something like this (after necessary coding):

The solution offered is from this question

And the query is as follows:

SELECT T2.id, T2.title,T2.controller,T2.method,T2.url
FROM (
    SELECT
        @r AS _id,
        (SELECT @r := parent_id FROM menu WHERE id = _id) AS parent_id,
        @l := @l + 1 AS lvl
    FROM
        (SELECT @r := 31, @l := 0) vars,
        menu m
    WHERE @r <> 0) T1
JOIN menu T2
ON T1._id = T2.id
ORDER BY T1.lvl DESC;

I need to adapt this query to work off a passed keyword, not an ID.


回答1:


Edit the vars subquery to have @r equal to the record_id of the row with the keywork, something like

SELECT T2.record_id, T2.title,T2.level,T2.keywords
FROM (SELECT @r AS _id
           , (SELECT @r := parent_id 
              FROM   tblAllCategories 
              WHERE record_id = _id) AS parent_id
           , @l := @l + 1 AS lvl
      FROM   (SELECT @r := record_id, @l := 0
              FROM   tblAllCategories
              WHERE  keywords like '%retro%') vars
           , tblAllCategories m
      WHERE  @r <> 0) T1
     JOIN tblAllCategories T2 ON T1._id = T2.record_id
ORDER BY T1.lvl DESC;

SQLFiddle demo

Having the keywork as a comma separated values is not the best, a many to many relationship between this table and a keyword table (with the compulsory junction table) will be better as it will avoid the use of LIKE. In this example if there were another category with the keyword 'retrobike' that category and all his hierarchy will also be in the result.




回答2:


This is going to take a while so get some coffee.

There are a lot of good resources available for hierarchical development. Most of what you will see below comes from sites like this and it refers you to Celko which I hardily recommend.

The first thing you'll have to do is remove the keywords field. The extra effort in development, use and maintenance is nowhere near the benefit received. I'll show you how to implement it later.

In this design, think of a row as a node. Each node has two values, the left boundary and the right boundary. These form a range or span of influence. If a node has boundaries of 1:4 and another node has 2:3, the second node is a subnode of the first as its span is contained in the span of the first. Also, as the boundaries of the second node are consecutive, there can be no node below it, so it must be a leaf node. This may sound complicated at first, especially when considering many levels of nodes, but you will see how the SQL is relatively easy to write and the maintenance effort for the table is minimal.

The complete script is here.

CREATE TABLE categories (
    id      INT not null auto_increment PRIMARY KEY,
    name    VARCHAR( 50 )   NOT NULL,
    lBound  INT             NOT NULL,
    rBound  INT             NOT NULL,
    -- MySQL does not implement check constraints. These are here for illustration.
    -- The functionality will be implemented via trigger.
    CONSTRAINT cat_ptr_incr_chk CHECK ( lBound < rBound ),  -- basic integrity check
    CONSTRAINT cat_ptr_root_chk CHECK ( lBound >= 0 )     -- eliminates negative values
);
create unique index ndx_cat_lBound on categories( lBound );
create unique index ndx_cat_rBound on categories( rBound );

Notice there is nothing here that say "I'm a leaf node", "I'm a root" or "My root node is such-and-such." This information is all encompassed by the lBound and rBound (left boundary, right boundary) values. Let's build a few nodes so we can see what this looks like.

INSERT INTO categories( name, lBound, rBound )
values( 'Categories', 0, 1 );

ID  name        lBound  rBound
==  ==========  ======  ======
 1  Categories       0       1

This we do before creating the triggers on the table. That's really so the insert trigger doesn't have to have special code that must recognize when the first row (the root node of the entire structure). That code would only be executed when the first row is inserted and never again. Now we don't have to worry about it.

So now me have the root of the structure. Notice that its bounds are 0 and 1. Nothing can fit between 0 and 1 so this is a leaf node. The tree root is also a leaf. That means the tree is empty.

So now we write the triggers and dml procedures. The code is in the script so I won't duplicate it here, just say that the insert and delete triggers will not allow just anyone to issue an Insert or Delete statement. Anyone may issue an Update, but only the name is allowed to be changed. The only way Inserts, Deletes and complete Updates may be performed is through the procedures. With that in mind, let's create the first node under the root.

call ins_category( 'Electronics', 1 );

This creates a node with the name 'Electronics' as a subnode of the node with ID=1 (the root).

ID  name        lBound  rBound
==  ==========  ======  ======
 1  Categories       0       3
 2  Electronics      1       2

Notice how the trigger has expanded the right boundary of the root to allow for the new node. The next node will be yet another level.

call ins_category( 'Televisions', 2 );

Node 2 is Electronics so the new node will be its subnode.

ID  name        lBound  rBound
==  ==========  ======  ======
 1  Categories       0       5
 2  Electronics      1       4
 3  Televisions      2       3

Let's create a new upper level node -- still it must be under the root, but will be the start of a subtree beside Electronics.

call ins_category( 'Antiques & Collectibles', 1 );

ID  name                    lBound  rBound
==  ==========              ======  ======
 1  Categories                   0       7
 2  Electronics                  1       4
 3  Televisions                  2       3
 4  Antiques & Collectibles      5       6

Notice the 5-6 does not fit between any boundary range except for the root. So it is a subnode directly under the root, just like Electronics, but is independent of the other subnodes.

The SQL to give a clearer picture of the structure is not complicated. After completing the tree with a lot more nodes, let's see what it looks like:

-- Examine the tree or subtree using pre-order traversal. We start at the node
-- specified in the where clause. The root of the entire tree has lBound = 0.
-- Any other ID will show just the subtree starting at that node.
SELECT  n.ID, n.NAME, n.lBound, n.rBound
FROM    categories p
join    categories n
    on  n.lBound BETWEEN p.lBound AND p.rBound
where   p.lBound = 0
ORDER BY n.lBound;

+----+----------------------------+--------+--------+
| id | name                       | lBound | rBound |
+----+----------------------------+--------+--------+
|  1 | >Categories                |      0 |     31 |
|  2 | -->Electronics             |      1 |     20 |
|  3 | ---->Televisions           |      2 |      9 |
|  4 | ------>Tube                |      3 |      4 |
|  5 | ------>LCD                 |      5 |      6 |
|  6 | ------>Plasma              |      7 |      8 |
|  7 | ---->Portable Electronics  |     10 |     19 |
|  8 | ------>MP3 Players         |     11 |     14 |
|  9 | -------->Flash             |     12 |     13 |
| 10 | ------>CD Players          |     15 |     16 |
| 11 | ------>2-Way Radios        |     17 |     18 |
| 12 | -->Antiques & Collectibles |     21 |     28 |
| 14 | ---->Furniture             |     22 |     27 |
| 15 | ------>Office Furniture    |     23 |     26 |
| 16 | -------->Chairs            |     24 |     25 |
| 13 | -->Art                     |     29 |     30 |
+----+----------------------------+--------+--------+

The output above is actually from a view defined in the script, but it shows clearly the hierarchical structure. This may easily be converted to a set of nested menus or navigational nodes.

There are enhancements that may be made, but they needn't change this basic structure. You'll find it reasonably easy to maintain. I had started out thinking this would be a whole lot easier in a DBMS such as Oracle, SQL Server or PostGreSQL which allows triggers on views. Then access could be limited to only the views so triggers would take care of everything. That would eliminate the need for separate stored procedures. But this way isn't half bad. I could happily live with it. In fact, there is a simplicity and flexibility to using the stored procedures that wouldn't be available thru views alone (you can't pass parameters to views).

The keyword feature is also defined but I won't show that here. Look at the script. Execute it a little at a time to get a clear picture of what is taking place. If you have any questions, you know where to find me.

[Edit] Added a few enhancements, including working with the keywords.



来源:https://stackoverflow.com/questions/24284995/adapt-query-to-return-category-hierarchy-by-keyword

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!