How to store big binary trees (about thousands in depth).
We tried to store in database, in table with rows: elem, parent, left_child, right_child
but it's very slow to handle this tree (to calculate, draw a part of it).
Which way you recommend? (calculating may be done in php). May be to store it in XML or json (text file) or in some matrices?
You could use an array or other linear store (e.g. relation id->node value) in the way that each node x has the children 2*x and 2*x+1. The problem might be that there are unused ids if the tree is unbalanced.
Sample:
2---4---8
/ \ \9
1-- 5---10
\ \11
3---6---12
\ \13
7---14
\15
Seems like writing to something like XML as you suggest would be best. Converting to XML and then back when you want to reload it would be pretty straight forward.
I use a structure like following
id parent tree slug name path children
1 0 1 root Root || |5|
2 7 1 child-1 Child 1 |1-5-8-7| |3|
It is super easy to query huge depth trees, because the query is linear.
There is a bit more data in each line, but that makes it quicker.
For example you can get a whole subtree with a simple query like:
$sql = 'SELECT * FROM `'.$this->_table_name.'` WHERE
`path` LIKE "%-:parent-%"
OR `path` LIKE "%|:parent-%"
OR `path` LIKE "%-:parent|%"
OR `path` LIKE "%|:parent|%"';
$result = $this->_db->custom_query($sql, array('parent' => $root->id));
And then build the depth array by a simple function:
private function _build_tree(Node $root, $data)
{
$mapped_node = array(
'node' => $root,
'subnodes' => array()
);
if ( !empty($root->children) )
{
foreach( $root->children as $child )
{
if ( array_key_exists($child, $data) )
{
$mapped_node['subnodes'][] = $this->_build_tree($data[$child], $data);
}
}
}
return $mapped_node;
}
NOTE! When I get back the children and path columns, I split them into arrays for PHP to be able to better work with them.
Related
When I create three-levels nested tree ( with only three entities ) that looks like this:
1 (lft 1, rgt:6)
-2 (lft 2, rgt:5)
-3 (lft 3, rgt:4)
and then I try to move node ( with id=3, i.e. ) from the third level to the second level as, let's say second child with this piece of code:
/* this line can be commented - it doesn't work with it either */ $chapter->setParent($parentEntity);
$repo->persistAsFirstChildOf($chapter, $parentEntity);
$repo->moveDown($chapter, 1);
As a result I got the tree that goes like this:
1 ( lft:-4, rgt:6 )
-3 (lft: 5, rgt:6)
-2 (lft 7, rgt:5)
instead of this:
1 (lft 1, rgt:6)
-2 (lft 2, rgt:5)
-3 (lft 3, rgt:4)
and child which should become second in order becomes first. As You can see, lft values aren't proper. Am I missing something?
You should update node and set new parent by gedmo TreeListener (get it by NestedTreeRepository->listener) :
<?php
class YourNestedTreeRepository extends NestedTreeRepository
.......
/**
* #param Node $node
* #param Node $newParent
*
* #return void
*/
public function setNewParent($node, $newParent)
{
$meta = $this->getClassMetadata();
$this->listener
->getStrategy($this->_em, $meta->name)
->updateNode($this->_em, $node, $newParent)
;
}
and then, anywhere in your code:
//set as first child of a new parent - Tree hierarchy, it doesn't touch ORM relation
$repo->setNewParent($node, $newParent);
//set new parent and save. It updates ORM relation only, doesn't touch Tree hierarchy
$node->setParent($newParent);
$entityProvider->save($node); // or $entityManager->flush()
//additionaly move it down
if ($yourCondition) {
$result = $repo->moveDown($node, $position);
}
You actually are OK there.
I think left/right values does not matter there since your order is still ok.
Your probleme is that you call
$repo->moveDown($chapter, 1);
This function will make your $chapter to move at next position (second one in your case).
Delete the call to moveDown and try it again.
As far as lft and rgt attributes are concerned, adding/removing node may recompute them.
If it's a real matter to you, then try calling (i'm not sur about this):
$repo->recover();
$em->flush(); // ensures cache clean
Actually guys, I've chosen bad tree type. Don't use nested when You want to change children position often.
I have a data table with 7 columns and 400 records. One of them is budget. I want to group the 400 rows by budget so that I get an array like this:
[budget]=>array(
[0]=>array(
[column1]=>'1',
[column2]=>'sand'
),
[1]=>array(
[column1]=>'2',
[column2]=>'clay'
)
)
[budget2]=>array(
[0]=>array(
[column1]=>'3',
[column2]=>'silt'
),
[1]=>array(
[column1]=>'4',
[column2]=>'stone'
)
)
So far I have been playing around with Yii's CdbCommand and CdbDataReader and PHP's PDOStatement but nothing is working right. I tried the following code
public function actionBidCostByBudget(){
$command = Yii::app()->db
->createCommand()
->Select('*')
->From('bid_cost')
# ->Query()
;
echo '<pre>';
echo get_class($command);
$pdostatement=$command->getPdoStatement();
if($pdostatement) echo get_class($pdostatement);
# print_r($statement->fetchall(PDO::FETCH_COLUMN|PDO::FETCH_GROUP));
# print_r($command->readall());
# print_r($statement->fetch());
# $columnsArray = BidCost::attributeLabels();
//print_r($rowsArray);
//$this->layout='\\layout';
}
The attempts to print_r all print out with nothing. getPdoStatement equals nothing. I have been trying to use PDO::FETCH_COLUMN|PDO::FETCH_GROUP as per the Php.net website, but it does not work either because I get nothing.
One of Yii's strengths is it's ActiveRecord, so why not use it?
Make your budget to a separate table (so you can generate a model from it). Reference it from your "datatable".
CREATE TABLE budget (
id INTEGER PRIMARY KEY,
name TEXT
);
CREATE TABLE datatable(
column1 TEXT,
column2 TEXT,
...
budget_id INTEGER,
FOREIGN KEY(budget_id) REFERENCES budget(id)
);
Next generate models with Gii, and now you can use your newly made relations like this:
$budget = Budget::model()->findByAttributes( ["name"=>"budget2"] );
foreach( $budget->datatables as $dt ) {
echo $dt->column1;
echo $dt->column2;
}
(I know. Not the array you asked for. Sorry if I'm way off with this.)
Alright, the bottom line is that I was not able to find a way to do this right thru Yii, so I did it with a more hands-on approach.
The first thing I did was basically initiate a database connection thru Yii.
$command = Yii::app()->db //outputs CDbConnnection
The next thing I did was get a PDO class from the connection:
$pdoinstance = $command->getPdoInstance(); //outputs PDO class
From this point, it was help obtained from PHP.net and another question posted on this forum:
$pdostatement=$pdoinstance->prepare('SELECT BUDGET_CODE,
PAY_ITEM, ITEM, DESCRIPTION FROM bidcost');
$pdostatement->execute();
//default fetch mode could not be set
# $pdostatement->setfetchmode(PDO::FETCH_GROUP|PDO::FETCH_ASSOC);
//returns array
$testarray=$pdostatement->fetchAll(PDO::FETCH_GROUP|PDO::FETCH_ASSOC);
Note: I am unfamiliar with terminology regarding tree structures. Please forgive any oversights that may be a result of my ignorance!
Practical Example
Given an array as such:
Array
(
[0] => 0
[1] => 2
[2] => 8
[3] => 9
)
The tree node with the key "9" would be found at $tree[2][8][9] (with 0 being the root). Given the above array, how would I construct a statement in PHP that would access the leaf node?
Target Code
/*
Let's say I am given a $leafNodeID of 9, and I'd like to save some
data ($dataToSave) into said leaf node
*/
$leafNodeID = 9;
$dataToSave = array("name" => "foobar");
$tree_path = $this->findPathToRootNode($tree, $leafNodeID); // This returns the array found above.
${?????} = $dataToSave; // <-- Here be dragons
Thanks in advance!
Edit: For those wondering, my findPathToRootNode function just recursively finds the parent node, and saves it in the array format found above. If there's a better way to represent said data (especially if it solves my problem), that would be even better.
Edit: On a read through, it seems this question is less about trees, but rather how to access an array given its structure in a separate array. Tagging as such.
Make a self-targeting function. This should do the trick (untested)
function getLeaf($tree, $targetleaf, $depth = 0){
if (isset($targetleaf[$depth+1])
return getLeaf($tree[$targetleaf[$depth]], $targetleaf, $depth + 1)
else
return $tree[$depth];
}
With $tree being the data, $tree the path to the array, and $depth speaks for itself.
Call the function with
$leaf = getLeaf($tree,$targetleaf);
I have some strings in a DB: x, y, z, x1, x2, x12, x22, x23, y1, y2, y3, z1, z2 (don't take them literally).
From those strings, and having some rules of classification (strings x1, x2 and x3 "belong" to string x), I want to build some kind of OOP-ish that incorporates those rules of classification.
x
x1
x12
x2
x22
x23
y
y1
y2
y3
z
z1
z2
Basically, I want to make a menu from plain strings, knowing their relation.
My question is: what is the best method to store their relation to each other, so that I can pin-point their exact "location" within this hierarchy? Is there a design pattern that suits this problem? I though of entities having some property "depth" or "type".
You could store menu items within a table having columns: id, title, parent_id
In PHP, you would then be able to have objects:
class MenuItem {
$id = 0;
$parent_id = 0;
$title = "";
$children = array();
}
You'll store children in the array using their ID as a key:
function add_child($menu_item) {
$this->children[$menu_item->id] = $menu_item;
}
When loading from the DB, you'll build your menu by inserting menu items in it. Simply have an insert function within the MenuItem object:
function insert($menu_item) {
if ($menu_item->parent_id==$this->id) {
$this->add_child($menu_item);
return true;
}
foreach ($children as $child) {
if ($child->insert($menu_item))
return true;
}
// return false if we could not insert it in any of our children
return false;
}
0:(x,null)
1:(x1,0)
2:(x12,1)
3:(x2,0)
4:(x22,3)
5:(x23,3)
[...]
'5:(x23,3)' means node 5 is a child of node 3 and contains 'x23'.
'0:(x,null)' means that node 0 is a top-node with no parent node.
this data-structure could be modeled by an numerical array that contains an array with two elements.
you have to parse that tree recursively.
One way to do this is by adding a parent_id column in the table. For each record, there will be a parent with value 0 or the primary id of some other record. Once you have this, you can write a recursive function to parse the tree.
you can organize these values in a table like a nested set tree (http://www.edutech.ch/contribution/nstrees/index.php), and then is fairly easy to find all children, or a parent
EDIT::
Maybe I should be asking what the proper way to get a result set from the database is. When you have 5 joins where there is a 1:M relationship, do you go to the database 5 different times for the data??
I asked this question about an hour ago but haven't been able to get an answer that was fitting. I went ahead and wrote some code that does exactly what I need but am looking for a better way to do it
This array gives me multiple rows of which only some are needed once and others are needed many times. I need to filter these as I have done below but want a better way of doing this if possible.
Array
(
[0] => Array
(
[cid] => one line
[model] => one line
[mfgr] => one line
[color] => one line
[orderid] => one line
[product] => many lines
[location] => many lines
)
[1] => Array
(
.. repeats for as many rows as were found
)
)
This code works perfectly but again, I think there is a more efficient way of doing this. Is there a PHP function that will allow me to clean this up a bit?
// these are the two columns that produce more than 1 result.
$product = '';
$orderid = '';
foreach($res as $key)
{
// these produce many results but I only need one.
$cid = $key['cid'];
$model = $key['model'];
$mfgr = $key['mfgr'];
$color = $key['color'];
$orderid = $key['orderid'];
// these are the two columns that produce more than 1 result.
if($key['flag'] == 'product')
{
$product .= $key['content'];
}
if($key['flag'] == 'orderid')
{
$orderid .= $key['content'];
}
}
// my variables from above in string format:
Here is the requested SQL
SELECT
cid,
model,
mfgr,
color,
orderid,
product,
flag
FROM products Inner Join bluas ON products.cid = bluas.cid
WHERE bluas.cid = 332
ORDER BY bluas.location ASC
Without seeing your database structure it's a bit hard to decipher how you actually want to manipulate your data.
Perhaps this is what you're looking for though?
SELECT p.cid, p.model, p.mfgr, p.color, p.orderid, p.product, p.flag, GROUP_CONCAT(p.content SEPARATOR ', ')
FROM products AS p
INNER JOIN bluas AS b ON p.cid = b.cid
WHERE b.cid = 332
GROUP BY p.cid, p.flag
ORDER BY b.location ASC
So now for each product cid each flag will have an entry consisting of a comma separated list instead of there being many repeating for each flag entry.
Then after you're done with the string you can quickly turn it into an array for further manipulation by doing something like:
explode(', ', $key['content']);
Again it's really hard to tell what information you're trying to pull without seeing your database structure. Your SQL query also doesn't really match up with your code, like I don't even see you grabbing content.
At any rate I'm pretty sure some combination of GROUP BY and GROUP_CONCAT (more info) is what you're looking for.
If you can share more of your database structure and go into more detail of what information exactly you're trying to pull and how you want it formatted I can probably help you with the SQL if you need.