-3

I have a table with products. Sub-products can be assigned to a main product.

products
________________________________________________________________________
id  |   product_title   |   main_id     | owner_id 
1   |   Volvo           |       0       | 1     
2   |   Mercedes        |       0       | 2                     
3   |   Chevrolet       |       0       | 1                     

4   |   Rear lights     |       1       | 1                     
5   |   Glasses         |       1       | 1                     
6   |   Seats           |       1       | 1                     
7   |   Heater          |       1       | 1                     
8   |   Radio           |       6       | 1                     
12  |   Tyres           |       6       | 1 
13  |   Rearview mirror |       8       | 1 
14  |   Door            |       8       | 1 
15  |   Engine          |       14      | 1 
15  |   Door            |       3       | 1 

I use function get_the_list(id = 0, owner_id = 1);


    function get_the_list(id = 0, owner_id = 1) {
        
        $query = "SELECT * FROM products WHERE main_id = $id  AND owner_id = $owner_id";
        while ($row = mysqli_fetch_array($result, MYSQLI_BOTH)) {
            
            $list .= $row[product_title];
            
            // select sub products from main_id
            $list .= get_the_list($row[main_id], 1);
        }
     
    }
echo get_the_list(id = 0, owner_id = 1);

On this way I get the whole product list. Works well, no problems.

(1)   i: 1 --- loop: 1, 1 - Volvo
        i: 1 --- loop: 2, 2 - Rear lights

        i: 2 --- loop: 3, 2 - Glasses

        i: 3 --- loop: 4, 2 - Seats
        i: 1 --- loop: 5, 3 - Radio
        i: 1 --- loop: 6, 4 - Rear-view mirror

        i: 2 --- loop: 7, 4 - Door
        i: 1 --- loop: 8, 5 - Engine

        i: 2 --- loop: 7, 3 - Tyres

        i: 4 --- loop: 6, 2 - Heater

____________________________

(2)   i: 2 --- loop: 1, 1 - Chevrolet
        i: 1 --- loop: 2, 2 - Door

____________________________


(3)   i: 3 --- loop: 1, 1 - Mercedes

____________________________


(4)   i: 4 --- loop: 1, 1 - XX

____________________________

First (number) is main_id. Second number is running $i++ in while. Third number should be continuous/ongoing counter. BUT this breaks after level 5. fourth number after comma is level.

I have to limit a select statement to only 8 products (incl. sub products). So I will end, for example, with rear-view mirror on this image example. It works. But it works not after more than 8, because the counter breaks.

How can I limit the number of products that can be retrieved, what select statement should I choose? OR WHAT php workaround?

enter image description here

  • Hi, it is not totally clear what you are asking – RiggsFolly Jun 20 '22 at 15:53
  • I have a table with products. Sub-products can be assigned to a main product. So I have a lot of products. However, someone may only be allowed to query a total of 20 products including the associated sub-products. My question is how I can solve this. Because it doesn't work in a while loop, because I don't know in the loop how many I have already queried. – Miedzegadze Jun 20 '22 at 15:58
  • So keep a record of how many you have processed – RiggsFolly Jun 20 '22 at 15:59
  • Well, the question is how. I call the select query function in the same function, but I can't break the while loop, evenwhen I have a record number. – Miedzegadze Jun 20 '22 at 16:13
  • I think you might be looking for an `ORDER BY id LIMIT 20` but again I am still not sure wuite which piece of code is doing what from your question – RiggsFolly Jun 20 '22 at 16:27
  • **Warning:** You are wide open to [SQL Injections](https://php.net/manual/en/security.database.sql-injection.php) and should use parameterized **prepared statements** instead of manually building your queries. They are provided by [PDO](https://php.net/manual/pdo.prepared-statements.php) or by [MySQLi](https://php.net/manual/mysqli.quickstart.prepared-statements.php). Never trust any kind of input! Even when your queries are executed only by trusted users, [you are still in risk of corrupting your data](http://bobby-tables.com/). [Escaping is not enough!](https://stackoverflow.com/q/32391315) – Dharman Jun 20 '22 at 16:50
  • Dharman, I use prepared statements. When I shorten my selects thus do not mean I use no prepared statements. And sorry, but my question is another one than prepare problems ... – Miedzegadze Jun 20 '22 at 17:31
  • I edited my post to make it clearer. – Miedzegadze Jun 20 '22 at 17:54

1 Answers1

0

First of all consider moving to PDO instead of mysqli. Then set main_id nullable and add a foreign key for it as products.main_id -> products.id.

The modern way to handle this task is using CTE.

I'not using the owner_id here and I assume you only have two levels in your products tree:

WITH root_products AS (
    SELECT
        products.id, 
        products.main_id,
        products.product_title,
        products.id as sequence_route
   FROM products WHERE main_id IS NULL OR main_id = 0
   LIMIT 0, 20 -- this is where limit is set
)
SELECT * FROM (
    SELECT * FROM root_products
    UNION 
    SELECT 
        id, 
        main_id, 
        product_title,
        CONCAT( main_id, '.', id ) sequence_route
    FROM products 
        WHERE 
            main_id IN ( SELECT id FROM root_products )
) result 
ORDER BY sequence_route

Note that this solution is using CTE. Check if your DB server version supports those. Tested with MySQL 8+

If you need some simplier queries, you will need two of those:

$query = "SELECT * FROM products WHERE main_id = 0 OR main_id IS NULL LIMIT 0, 20";

This will fetch 20 top-level products. And:

$query = "SELECT * FROM products WHERE main_id IN( " . implode(',', $root_ids_you_take_from_the_prev_query ) . " )";

will get the subproducts.

Upd:

Here's the query for unlimited depth. Its is still tied to a root product with id = 1, you might want to leave this in place for performance reasons or rework the WHERE clause to main_id IS NULL at the first SELECT to grab all the root entries. This query implements 'way #2'. It is still all about an ORDER BY. If you need to roll back to a layer-by-layer 'way #1', just drop the ORDER BY clause and corresponding rank/recursion_level columns from CTE.

-- This CTE will take the given root and 
-- recursively grab all its descendants, 
-- layer after layer
WITH RECURSIVE deeper_products AS (
    SELECT
        0 as recursion_level                  -- This is required for ranking
        , CAST(id AS CHAR) as `rank`          -- This should be 'CAST(id AS FLOAT)' but i'm 2 lazy to update my mysql installation
                                              -- Server makes conversions internally just fine anyways

        , CAST(id as CHAR) as sequence_route  -- Just a visual helper, does nothing

        , id
        , main_id
        , product_title
    FROM products
        WHERE id = 1 -- the root product id
    UNION 
        SELECT    
            prev_layer.recursion_level + 1 as recursion_level
            , prev_layer.rank + products.id / POWER( 100, prev_layer.recursion_level + 1 ) as `rank` -- I assume there'r less than 100 children there
            , CONCAT( prev_layer.sequence_route, '.', products.id ) sequence_route

            , products.id 
            , products.main_id
            , products.product_title
            

        FROM products 
        INNER JOIN deeper_products prev_layer
            ON products.main_id = prev_layer.id
)


SELECT * FROM deeper_products
ORDER BY `rank` -- Remove this and the rank/recursion_level fields from CTE if you need a level-by-level ordering
LIMIT 0, 8      -- Your limit

Upd2: Here's a PHP solution. I'm using PDO here (messing with mysqli is a pain). No recursion needed, so the limit is calculated pretty simple:

<?php
$pdo = new PDO("mysql:dbname=test;host=localhost", 'test', 'SECRET_PASSWORD');

function get_list( $dbh, $pid = null ){

    $out = [];
    $limit = 8;

    // The regular query - items never have NULL as their main_id
    $regular_query = "SELECT * FROM products WHERE main_id = ? LIMIT 0, $limit";

    // The root query - items might have NULL as main_id
    $root_query = (
        is_null( $pid )
        ? "SELECT * FROM products WHERE main_id IS NULL LIMIT 0, $limit"
        : $regular_query
    );

    // Results stack contains statements
    // Every deeper level will add a new statement the top
    $root_sth = $dbh->prepare( $root_query );
    $root_sth->execute( [ $pid ] );
    $results_stack = [ $root_sth ];

    // Fill in the final result, loop untill the limit reached
    // or while we still have records in DB
    while( count( $out ) < $limit and count( $results_stack ) ){

        // Take the topmost statement..
        $sth = $results_stack[ count( $results_stack ) - 1 ];

        // .. and grap the row 
        $row = $sth->fetch(PDO::FETCH_BOTH);

        // The row is there, so we..
        if( $row ){
            // ..save it in the final result..
            $out[] = $row;

            // ..and add a new query returning children of a 
            // current item on the top of a stack
            $regular_sth = $dbh->prepare( $regular_query );
            $regular_sth->execute( [ $row['id'] ] );
            $results_stack[] = $regular_sth;
        }
        // No row - we'r out of data with this query, remove the it from the stack
        else{
            array_pop( $results_stack );
        }
    }

    return $out;

}

print_r( get_list( $pdo ) );


Upd3:

This builds the UL structure. I assume you've added the depth key for the items:

<?
$data = [
    [ 'title' => 'Item 1', 'depth' => 1 ],
    [ 'title' => 'Item 2', 'depth' => 1 ],
    [ 'title' => 'Item 2.1', 'depth' => 2 ],
    [ 'title' => 'Item 2.2', 'depth' => 2],
    [ 'title' => 'Item 2.2.1', 'depth' => 3 ],
    [ 'title' => 'Item 2.2.2', 'depth' => 3 ],
//  [ 'title' => 'Item 3', 'depth' => 1 ],
];
?>
<ul>
<?
foreach( $data as $i => $row ){

    ?><li><?=$row['title']?><?

    // If we even have a next level
    if( !empty( $data[$i + 1]) ){
        // If next item is on a level higher - open ul tag for it
        if( $data[$i + 1]['depth'] > $row['depth'] ){?>

            <ul>
        <?} 
        // If next item is a level lower - close li/ul tags between the levels and close higher li
        elseif( $data[$i + 1]['depth'] < $row['depth'] ){?>
            <?=str_repeat('</li></ul>', $row['depth'] - $data[$i + 1]['depth'] )?>
            </li>
        <?} 
        // Next item is on the same level, just close self li and go on
        else{?>
            </li>           
        <?}
    }
    // This is the last item, close li/ul to the end
    else{
        print str_repeat("</li></ul>", $row['depth'] );
    }
}?>
Jared
  • 1,294
  • 2
  • 8
  • 13
  • I edited my post to make it clearer, including an image (link). I have up to 5 Levels. And I may stop after 8 products WITH sub products. That means, I cant first select 20 main products and THEN some subs. I first loop a main product with subs, if these are not 8, then next main with subs and so on ... – Miedzegadze Jun 20 '22 at 18:07
  • No it is not clear anyways. So with the structure from your image the result should be: ` 1. Volvo, ...... , 1. Volvo / 1.3 Seats / 1.3.1 Radio / 1.3.1.1 Rear-view mirror ` ? But these are 6 products in total including the root one. Or should it be ` 1. Volvo, ...... , 1. Volvo / 1.3 Seats / 1.3.1 Radio / 1.3.1.1 Rear-view mirror [ took 5 subproducts and getting back to root] 2. Mercedes, [five Mercedes subs here], [other root elements with 5 children, up to 20 of roots] `? ``` – Jared Jun 20 '22 at 18:35
  • It should be: ` 1. Volvo, 2. Rear lights/glasses/seats/heater , 3. Radio / Tyres , 4. Rear-view mirror ` – Miedzegadze Jun 20 '22 at 18:53
  • 4 levels, stop on 8. for example. And: big thanks that you think about it. ;) Mercedes is number 11, Chevrolet than 12, Chevrolet/Door 13 ... – Miedzegadze Jun 20 '22 at 18:54
  • So the last item on the result set should be `1.3.1.1 Rear-view mirror`, right? Does your MySQL support CTE? – Jared Jun 20 '22 at 19:00
  • Yes, you are right. No, no support CTE. PHP 7.4, 10.5 (MariaDB) – Miedzegadze Jun 20 '22 at 19:05
  • MariaDB supports CTE from version 10.2.1 And you need the subs from a particular root element (seven of subs for our example)? Ill update the answer a bit later. – Jared Jun 20 '22 at 19:18
  • Then I have to try CTE, when there is no "easier" way? I have two images here => https://postimg.cc/gallery/R9gbyLj There are two ways two get product 8, and no matter which one, important is only that it will be number 8 from a (while) loop. But I would prefer "way number 2" on images, it should be more native(?). – Miedzegadze Jun 20 '22 at 19:34
  • Updated the answer for your case. The "easier" way involves a PHP recursion or a cycle with a stack. Generally I'd prefer a pure SQL solution cause it is actually simpler and should be faster with correctly indexed and not too large tables (hundreds to thousands records) . And roll back to a PHP solution only if I had performance issues. – Jared Jun 20 '22 at 23:02
  • First, thank you for your time and help, I appreciate it very much. Unfortunately, the query does not work for me. According to my provider, recursive is not possible, I would have to switch to postgresql, but I'm already too far along for that ... Just for info: in phpmyadmin there is error #1064. When i execute query directly via script, there is error Warning: mysqli_fetch_array() expects parameter 1 to be mysqli_result, bool given. I double checked all ... I guess because the provider limits mariaDB ... So unfortunately I have to keep looking for a recursive function with PHP. – Miedzegadze Jun 21 '22 at 05:31
  • Edited the question. More information specified. – Miedzegadze Jun 21 '22 at 08:12
  • @ Jared, nother question: you "use deeper_products" and "products" in your query. Did that specially mean you have two tables in use? Because I only have one table, which I query with subquery, with that recursive function (updated question). – Miedzegadze Jun 21 '22 at 08:33
  • The `deeper_products` is a name for a dynamic table that is created with CTE. It is filled via the `WITH` clause, first `SELECT` inserts initial records there and the second one (right after the `UNION`) inserts the rest in a cycle untill it returns no rows. The second `SELECT` may refer that named table (with the `INNER JOIN` in this case), what makes the whole construction recursive. This dynamic table can be refered later in the main query and is going to be destroyed after the outermost query is complete. – Jared Jun 21 '22 at 11:37
  • Added a PHP solution to the answer. – Jared Jun 21 '22 at 12:19
  • @ Jared, you deserve a bag full of karma points. Really big thank you for your help and that you do not lecture about mistakes as others do here often. Your php solution works like a charm and safed many time for me. No problem with PDO. I even start learning about CTE, it sounds interesting, will dive deeper into that. THANKS! – Miedzegadze Jun 21 '22 at 16:55
  • Glad it helped. GL – Jared Jun 21 '22 at 20:38
  • @ Jared, all works well. But unfortunately it is now no longer possible for me to build up the ul/li structure correctly, because I can no longer see in the loop when which subcategory ends in order to close the main ul. you can see this structure in my two pictures. do you have any ideas or help? I use this whole list for such a list (first one) => https://camohub.github.io/jquery-sortable-lists/index.html – Miedzegadze Jun 23 '22 at 08:46
  • Its is still possible. Instantinate a `$depth = 0;` variable. Save its value into each row. Increase it every time you put a new statement on a stack and decrease when we remove statement. Later when you build your menu open `
      ` every time current item's `depth` is more than the prev's item, and close every time current item's depth is less than the prev's item. And in the end put as much closing `
    ` tags as the `depth` of the last item is.
    – Jared Jun 23 '22 at 11:39
  • That works till the highest depth, then it gets broken. The next item on lowest depth gets not it right lowest positin, it stucks higher. The end put closing do not repair this. – Miedzegadze Jun 23 '22 at 14:21
  • Building tree markup structure from a plain pseudo-tree might be a bit tricky. You might want to build a real tree while querying DB via replacing `$out[] = $row;` with something like `$all_the_records[ $row[ 'main_id' ] ][ '_children' ][] = &$row;` for non-root items to recursively build your markup from that (adding roots references to `$roots` array). Anyways ill update the answer with a code that builds your `UL` structure from the plain array. – Jared Jun 23 '22 at 15:42
  • @ Jared, I got everything working right now, even added ;charset=UTF8 to PDO connection, took me some time to figure out that, but now all is like it should. Unfortunatelly one can't send here more than words with big thanks, I would send you a gift for your help if I could! THANKS AGAIN! – Miedzegadze Jun 28 '22 at 10:08