I have created a recursive function to get parent products of a product. I'm almost sure I'm nearly there, but need help snapping out of recursion.
The result I am looking for is something like:
Product 1 (id:1, parent:none)
Product 2 (id:2, parent:1)
--- --- Product 3 (id:3, parent:2)
--- --- Product 4 (id:4, parent:2)
--- Product 5 (id:5, parent:1)
--- --- Product 6 (id:6, parent:5)
--- --- Product 7 (id:7, parent:5)
My updated function is as follows:
function get_parents ($pid, $found = array()) {
array_push ($found, $pid);
$sql = "SELECT * FROM products WHERE child_id = '$pid'";
$result = mysql_query($sql) or die ($sql);
if(mysql_num_rows($result)){
while($row = mysql_fetch_assoc($result)){
$found[] = get_parents($row['pid'], $found);
}
}
return $found;
}
I call it using a simple:
$parents = get_parents($pid);
The problem I am having is that when I run it, it creates an infinite loop, which doesn't break.
I don't want to get done for spamming so I have saved the result of my array to a text file, which can be seen here http://vasa.co/array.txt
Any help would be seriously appreciated :-)
Hmm.. judging by your structure of your DB, it would seem that something is amiss unless I'm missing something
The statement
$sql = "SELECT * FROM products WHERE child_id = '$pid'";
Tells me that for each product, you are storing the ID of the child. Typically, in a tree based structure, it's the reverse, you store the parent ID not the child - unless you want a child node to have many parents. If that is the case, then the function could easily run into problems. Consider the following:
| ID | Child_ID |
+----+----------+
| 1 | 2 |
| 2 | 1 |
This would cause an infinite loop. If you store the parent_id, then by that nature, you are encoding the graph to be hierarchical. Since every product has A parent, then the logic can be written recursively.
The could then be written as such?
function get_parents ($pid, $found = array()) {
array_push ($found, $pid);
$sql = "SELECT * FROM products WHERE id = '$pid'";
$result = mysql_query($sql) or die ($sql);
if(mysql_num_rows($result)){
while($row = mysql_fetch_assoc($result)){
$found[] = get_parents($row['parent_id'], $found);
}
}
return $found;
}
Related
I have a table in database and in this table i have added 3 columns that is i, name,parent_id.please see below.
ID | name | parent_id
1 name1 0
2 name2 1
3 name3 1
Now i want to fetch this id from database. I have created a method in PHP and fetch data one by one from database. Please see below
function getData($id)
{
$array = array();
$db = JFactory::getDBO();
$sql = "SELECT * from table_name when id = ".$id;
$db>setQuery($sql);
$fetchAllDatas = $db->getObjectList();
foreach ($fetchAllDatas as $fetchAllData)
{
if($fetchAllData->parent_id > 0)
{
$array[$fetchAllData->parent_id] = $fetchAllData->name;
$this->getData($fetchAllData->parent_id);
}
else
{
$array[$fetchAllData->parent_id] = $fetchAllData->name;
}
}
return $array;
}
Now if i call this method with id 3 like
$this->getData(3); // has a parent
It will return like that
Array(
[0]=>name1
)
But i want like below
Array(
[1]=>name3,
[0]=>name1
)
I know i have redefine array if we have parent but how i manage it.
i have used array_push php function but its not work with my condition.
foreach ($fetchAllDatas as $fetchAllData)
{
$array[$fetchAllData->parent_id] = $fetchAllData->name;
if($fetchAllData->parent_id > 0)
array_push($array,$this->getData($fetchAllData->parent_id));
}
return $array;
1)Because you do $array[$fetchAllData->parent_id] = $fetchAllData->name; in the if and in the else, you do this in both cases so put out of if..else.
2) Try to push the result of your second call in the original array to get what you want.
you have unique ID in your table, so if you call your query it will always return only one result. Maybe you wanted to write query this way:
$sql = "SELECT * from table_name when parent_id = ".$id;
if you want to get the result with given ID and his parent, you should add this after calling $this->fetchSharedFolder(...);
$array = array_merge($array, $this->getData($fetchAllData->parent_id));
I am trying to get direct parent (if exists also not grand parents) of each child element in order, so far I have tried this function:
$child_id = 15;
$query = "SELECT * FROM family WHERE parent_id < $child_id ORDER By parent_id DESC LIMIT 0,7";
$res = mysql_query($query);
while($obj = mysql_fetch_assoc($res))
{
$results[] = $obj;
}
$fetchedResults = fetchParentsById($results, $child_id);
function fetchParentsById($results, $id, $parentfound = false, $parents= array())
{
foreach($results as $row)
{
if((!$parentfound && $row['parent_id'] == $id) || $row['child_id'] == $id && $row['parent_id'] > 0)
{
$rowdata = array();
foreach($row as $k => $v)
{
$rowdata[$k] = $v;
}
$parents[] = $rowdata;
if($row['child_id'] == $id)
{
$parents= array_merge($parents, fetchParentsById($results, $row['parent_id'], true));
}
}
}
return $parents;
}
Now, I get all the parents but I want to get parents till the desired level from bottom up, e.g. I have 15 records, if I give child_id=15 and level = 4, then it should get me (1) parent of 15 (2) parent of 14 (3) parent of 13 (4) parent of 12 and should not proceed.
Iow, I just have to limit the number to fetch the parents to the desired level.
Any better solution, or where am I mistaking?
Table Structure :
id | parent_id | child_id
First of all, i was thinking your table structure is wacky. Until i realized that its a cross-table and id is not what parent_id or child_id refer to.
Assuming there is only one parent per child, i would advise structuring your table simpler:
| id | parent_id |
And use NULL for parent_id to indicate an id has no parent. The query would simply be
SELECT * FROM family WHERE parent_id = NULL
Or if the data has fixed id's that may or may not appear in the data, use a query like
SELECT * FROM family WHERE parent_id <> (SELECT id FROM family GROUP BY id)
to find any parent_id that is not listed in the table as id. But that would assume the data is incomplete.. which it should never be unless you dont have to trust your data.
I'd take a look at Closure Sets.
Read from slide 48 on for a few ways of working with Parent / Child data.
http://www.slideshare.net/billkarwin/sql-antipatterns-strike-back
You'll need to restructure your data - whether it's worth it (or not) depends on the volume of queries you expect.
Namastey!
I'm currently working on one of my projects where I get to one doubt.
What I'm doing is:
I have got 4 tables called:
1: project_degree
2: project_department
3: project_category
4: Projects
project_degree have got four top level degrees which are: B.tech, M.tech, M.C.A. & M.B.A.
project_department have got their respective departments such as: C.S.E, I.T., E.E.E. and so on.
Now, project_category have some categories like PHP, .NET, SAP etc.
What I'm trying to do is, I'm adding a project to database called "projects" where I'm assigning it to its degree, department and category.
Structure of projects table is as follows:
id | project name | degree_id | Dept_id | Cat_id
1 | Sample project | 1 | 3 | 4,3,6,5,9
Now as you can see above, field "cat_id" have got multiple categories where each categories ID have been separated by a comma.
Now when I call a query get all projects from the table called "projects", I want to list of categories related to that particular project and my code goes like this:
$query = mysql_query("SELECT * from projects ORDER BY id DESC");
while($row = mysql_fetch_array($query)) {
$categories = $row['cat_id'];
$cat_arr = explode(',',$categories);
$cat_size = sizeof($cat_arr);
for($i=0;$i<$cat_size;$i++) {
$get_cat = mysql_query("SELECT * from project_category WHERE id='".$cat_arr['$i']."'");
$cat_row = mysql_fetch_array($get_cat);
echo $cat_row['name']; // Here finally I'm getting categories name
}
}
So, finally I'm running nested loop and two queries to get a category name. I doubt it affect the page load time when it comes to huge data.
Is there any better alternative to this situation?
Here should be work
$query = mysql_query("SELECT * from projects ORDER BY id DESC");
while($row = mysql_fetch_array($query)) {
$get_cat = mysql_query("SELECT * from project_category WHERE id IN (" . $row['cat_id'] . ")");
$cat_row = mysql_fetch_array($get_cat);
echo $cat_row['name']; // Here finally I'm getting categories name
}
But normalization is better.
It's better to add a new table, project_has_catID where you can store project.id and cat.id as two columns (project_id and cat_id). Remove the Cat_id column in the project table.
Once done, simply select the name by the next query;
$query = mysql_query("SELECT project_category.name from project_category INNER JOIN project_has_catID phc ON phc.cat_id = project_category.id ORDER BY phc.project_id DESC");
By this, you have a list of category names. You don't have to loop with multiple database communications.
You could load all category names into an associative array first, then look up category names in this array when looping through the projects.
$categoryNames = array();
$query = mysql_query("SELECT id, name FROM project_category");
while ($row = mysql_fetch_array($query))
{
$categoryNames[$row['id']] = $row['name'];
}
$query = mysql_query("SELECT * FROM projects ORDER BY id DESC");
while ($row = mysql_fetch_array($query))
{
$categoryIds = explode(',', $row['cat_id']);
foreach ($categoryIds as $cat_id)
{
$cat_name = $categoryNames[$cat_id];
//do what you want with the name here
}
}
This way, you don't have to change your database structure, or the structure of your code. And you only have to execute two queries in total, so this is by far the simplest solution.
Needless to say, normalization is always better as others have indicated (never use a column that contains multiple comma-separated values), but you probably knew that and had your reasons for setting up your database tables this way.
I have an table structure like this
mysql> SELECT id, name, parent_id FROM categories;
+-------+------------+-----------+
| id | name | parent_id |
+-------+------------+-----------+
| 15790 | Test | 0 |
| 15791 | Test2 | 0 |
| 16079 | Subtest | 15790 |
| 16080 | Subtest 2 | 15790 |
| 16081 | Subsubtest | 16079 |
+-------+------------+-----------+
Now I want to look up the parent for every children and sibling and give it back in the right order for deletion.
So my output in this case would be:
Array
(
16081,
16080,
16079,
15791,
15790
)
I can't delete just by reversing the parent ids, because this should be solid walking back the tree.
Also I am not able/allowed to change the structure of the table. So building kind of an index is necessary.
Assuming you don't have access to TRUNCATE, SET (so you could do SET FOREIGN_KEY_CHECKS=0;), ALTER, etc. etc., and absolutely must use a script:
Since the question is tagged with php, this should do the trick:
function reversetree($src_arr, $currentid = 0)
{
$cats = array();
foreach($src_arr as $id => $parent)
{
if($parent == $currentid)
{
$cats[] = $id;
$cats = array_merge($cats, reversetree($src_arr, $id));
}
}
return !$currentid ? array_reverse($cats) : $cats;
}
$rs = array();
foreach($pdo->query('SELECT id, parent_id FROM categories') as $row)
$rs[$row['id']] = $row['parent_id'];
$stmt = $pdo->prepare('DELETE FROM categories WHERE id = ?');
$pdo->beginTransaction();
foreach(reversetree($rs) as $v)
$stmt->execute(array($v));
$pdo->commit();
I don't understand why you need the IDs in a particular order. You can delete them with a transaction and they will all be deleted simultaneously.
DELETE FROM categories WHERE ID IN (15790,15791,16079,16080,16081);
You could add FOREIGN KEY constraint with CASCADE on DELETE.
The foreign key will point to the same table on the parent id field.
When you delete the parent, all the children (no matter what level) are removed automatically.
<?php
// housekeeping
$pdo = new PDO($dsn, $user, $password);
$select = $pdo->prepare(
"SELECT parent.id AS parent_id, child.id AS child_id
FROM categories AS parent
JOIN categories AS child ON parent.id = child.parent_id
WHERE parent.id = ?"
);
$delete = $pdo->prepare('DELETE FROM categories WHERE id = ?');
// deletes $node_id, deletes its children first if required
function delete_node($node_id){
$select->execute( array($node_id) );
$children = $select->fetchAll(PDO::FETCH_NUM);
if (count($children) !== 0) { // if 0, then the category does not exist, or it has no child
foreach ($children as $child) { // call delete_node() recursively on each child
delete_node ($child[1]);
}
}
$delete->execute( array($node_id) ); // then delete this node (or do nothing if $node_id does not exist)
}
// to delete one category and all its sub-categories
delete_node(15790);
// to delete all contents
$allTopLevel = $pdo->exec('SELECT id FROM categories WHERE parent_id = 0')->fetchAll(PDO::FETCH_NUM);
foreach ($allTopLevel as $node) {
delete_node($node[0]);
}
Not tested, not even sure if it "compiles", but you get the idea. Make sure to lock the table (or start a transaction) before calling delete_node().
Sorry, i can't help much, because SQL is not my thing. But perhaps someone could transfer java pseudo code to the solution
delete(table.getFirstRow().get(id));
delete(id_a){
for(data_set : table)
if(data_set.get(parent_id) == id_a)delete(data_set.get(id));
}
table.remove(id_a);
}
edit: no iteration about elements? So something like this?
delete(list){
if(list.size() == 0)return;
idList = list.getAll(id);
plist = table.getAllWhichEquals(parent_id, idList);
delete(plist);
table.remove(idList);
}
ah, forget it, i'm deleting not all at the same time, was just a try ^^
My function looks like that. It works but does lots of work (recursively calls itself and does lots of db queries). There must be another way to do same thing but with array (with one query). I can't figure out how to modify this function to get it work with array.
function genMenu($parent, $level, $menu, $utype) {
global $db;
$stmt=$db->prepare("select id, name FROM navigation WHERE parent = ? AND menu=? AND user_type=?") or die($db->error);
$stmt->bind_param("iii", $parent, $menu, $utype) or die($stmt->error);
$stmt->execute() or die($stmt->error);
$stmt->store_result();
/* bind variables to prepared statement */
$stmt->bind_result($id, $name) or die($stmt->error);
if ($level > 0 && $stmt->num_rows > 0) {
echo "\n<ul>\n";
}
while ($stmt->fetch()) {
echo "<li>";
echo '' . $name . '';
//display this level's children
genMenu($id, $level+1, $menu, $utype);
echo "</li>\n\n";
}
if ($level > 0 && $stmt->num_rows > 0) {
echo "</ul>\n";
}
$stmt->close();
}
You can build a tree-based array fairly easily, so it'd be one single query and then a bunch of PHP logic to do the array building:
$tree = array();
$sql = "SELECT id, parent, name FROM menu WHERE parent ... etc.... ";
$results = mysql_query($sql) or die(mysql_error());
while(list($id, $parent, $name) = mysql_fetch_assoc($results)) {
$tree[$id] = array('name' => $name, 'children' => array(), 'parent' => $parent);
if (!array_key_exists($tree[$parent]['children'][$id])) {
$tree[$parent]['children'][$id] = $id;
}
}
For this, I'm assuming that your tree has a top-level '0' node. if not, then you'll have to adjust things a bit.
This'd give you a double-linked tree structure. Each node in the tree has a list of its children in the ['children'] sub-array, and each node in the tree also points to its parent via the ['parent'] attribute.
Given a certain starting node, you can traverse back up the tree like this:
$cur_node = 57; // random number
$path = array();
do {
$parent = $tree[$cur_node]['parent'];
$path[] = $parent;
$cur_node = $parent;
} while ($parent != 0);
I think the first thing you can fix is removing the WHERE parent = ? clause and then work on the resulting query result, this will make you work a bit more in managing the result but will definitely safe you IO operations.
Using parts of Marc B Solution
$tree = array();
$sql = "select id, parent, name FROM navigation AND menu=? AND user_type=?";
$results = mysql_query($sql) or die(mysql_error());
while(list($id, $parent, $name) = mysql_fetch_assoc($results)) {
$tree[$id] = array('name' => $name, 'children' => array(), 'parent' => $parent);
if (!array_key_exists($tree[$parent]['children'][$id])) {
$tree[$parent]['children'][$id] = $id;
}
}
print_r($tree);
Replace the ? with the actual values, and give that a run, what is your output?
Maybe not what you wanted, but it is great when it comes to trees.
You would have to rebuild your table and had some code to output the html, but you would have only one query. It could be worth the effort on the long run.
ie.If you have this menu
# Menu hierarchy:
- Home
- Product
|- Tv
|- Radio
- About us
It would looks like this in the db.
+----+----------+-----------+-----+-----+
| id | menu | parent_id | lft | rgt |
+----+----------+-----------+-----+-----+
| 1 | Home | null | 1 | 2 |
+----+----------+-----------+-----+-----+
| 2 | Product | null | 3 | 8 |
+----+----------+-----------+-----+-----+
| 3 | Tv | 2 | 4 | 5 |
+----+----------+-----------+-----+-----+
| 4 | Radio | 2 | 6 | 7 |
+----+----------+-----------+-----+-----+
| 5 | About us | null | 9 | 10 |
+----+----------+-----------+-----+-----+
The data could be fetch using a similar query
$select = "SELECT * FROM table_name WHERE lft BETWEEN 3 AND 8;"
To output a specific menu:
- Product
|- Tv
|- Radio
I know its not exactly the answer your were looking for, but FYI, there are other ways to use hierarchical tree data.
Good luck.
I wrote in the past an ugly way but with simple SELECT:
I store in text/varchar field strings like this:
/001
/001/001
/001/002
/002
/002/001
/002/001/001
Ignore the hebrew and look in window.aMessages array, to look how it works:
http://www.inn.co.il/Forum/Forum.aspx/t394009#4715854