This question already has answers here:
Generating Depth based tree from Hierarchical Data in MySQL (no CTEs)
(4 answers)
Closed 7 years ago.
Imagine an comment system with an infinite reply structure. Like this:
Comment
Reply to comment
Reply to reply to comment
Reply to reply to reply to comment
etc....
Reply to comment
Comment
Reply to comment
Like reddit has:
I am trying to think of a structure inside PHP combined with mysql to do this. I thought of something like this:
$query = mysqli_query($link, "SELECT * FROM comments");
while($comment_array = mysqli_fetch_assoc($query)){
echo $comment_array['text'];
$query_reply = mysqli_query($link, "SELECT * FROM comments WHERE reply_id='$comment_array[id]'");
while($reply_array = mysqli_fetch_assoc($query_reply)){
echo $reply_array['text'];
$query_reply2 = mysqli_query($link, "SELECT * FROM comments WHERE reply_id='$reply_array[id]'");
while($reply_array2 = mysqli_fetch_assoc($query_reply2)){
echo $reply_array['text'];
...... etc.
}
}
}
But as you can see there is a problem in this structure. This structure is not infinite and the same bit of code must be repeated a lot of times.
Is there a way to do this more efficiently? Putting a loop somewhere? Making a function, like searchRepliesofComment();?
There are a few ways to handle this without unbound recursion. A recursive function calling the DB repeatedly is not a good idea, even for reads. A few calls in a test environment may only take a few milliseconds each but even a little traffic can bring the DB to its knees when each request is firing off tons of DB hits.
First option you could have a post_id,parent_reply_id on all replies. The post_id would allow to select all replies that apply to a post but would have no hierarchy. Then you would build the reply tree in memory using parent_reply_id. Replies with a null parent_reply_id would represent top level replies. You could build this map easily with "SELECT * FROM replies WHERE post_id = ? ORDER BY parent_reply_id ASC". Ordering this way will make building the tree really straight forward.
You can also use the nested set model. Here is a great example. It does incur the cost of having to do a bulk (but relatively) light write operation on many replies when a new reply is added but the benefit is that you can build the tree with a single mysql query.
You will want to build what's known as a recursive function
function getComments($link, $parent = null)
{
$sql = "SELECT * FROM comments";
$sql .= $parent ? " WHERE reply_id=".(int)$parent : null;
$query = mysqli_query($link, $sql);
$results = array();
while ($result = mysqli_fetch_assoc($query)) {
if ($children = getComments($result['id'])) {
$result['children'] = $children;
}
$results[] = $result;
}
return $results;
}
function renderComments(array $comments)
{
$output = '';
foreach ($comments as $comment) {
$output .= $comment['text'];
if (isset($comment['children'])) {
$output .= renderComments($comment['children']);
}
}
return $output;
}
Now you can render your comments like:
renderComments(getComments($link));
Related
This question already has answers here:
Why shouldn't I use mysql_* functions in PHP?
(14 answers)
Closed 2 years ago.
I am trying to learn as I go so I don't reach out for help until I've exhausted all other avenues. I would appreciate some help/direction here because one of the solutions I found on Stack Overflow (near the bottom of the page) does not seem to work for me. After several hours, I'm ready to throw in the towel! I've found references informing me that various extensions have been depreciated and/or removed altogether over time so I'm now wondering if this code snippet is older and doesn't run for a different reason (although I'm aware that I might also be missing something). As reference I am on a shared server so I can't change the PHP version (5.6.40) or the mySQL version (5.6.46).
My table is named (for testing) test_json. I only have three columns: id, parent, name. The adjacency data is max 4-deep. I am trying to build an array that I can then convert (I'm thinking json_encode($arr)). Based on the code snippet I copied/paste/modified, I thought it would work but I get nothing on the screen even after using print_r and var_dump. In Chrome dev tools there are no connection errors.
Here is the code I'm using:
echo '<pre>';
$categories = OrgChart::getTopCategories();
print_r($categories);
echo '</pre>';
class OrgChart
{
public static function getTopCategories()
{
return self::getCategories('parent = 0');
}
public static function getCategories($where = '')
{
if ($where) $where = " WHERE $where";
$result = mysql_query("SELECT * FROM test_json $where");
$categories = array();
while ($category = mysql_fetch_array($result)){
$my_id = $category['id'];
$category['children'] = OrgChart::getCategories("parent = $my_id");
$categories[] = $category;
}
mysql_free_result($result);
return $categories;
}
}
Updates after several hours of exploring options:
Christos and Dharman were right and the code I attempted to use had a number of depreciated (removed?) extensions. Now that I've fixed those and have zero errors (using error_reporting(E_ALL); ini_set("display_errors", 1); ini_set('display_startup_errors', 1);) I'm still stymied why I only get this output:
Array
(
)
from the code below. Any other suggestions on what I'm missing? Thank you for any help that can be provided. I could be looking right at it but my eyes are starting to cross.
echo '<pre>';
$categories = OrgChart::getTopCategories();
print_r($categories);
echo '</pre>';
class OrgChart
{
public static function getTopCategories()
{
return self::getCategories('parent = 0');
}
public static function getCategories($where = '')
{
if ($where) $where = " WHERE $where";
// $result = mysqli_query(&db, "SELECT * FROM test_json $where");
Database::initialize();
$result = mysqli_query (Database::$conn, "SELECT * FROM test_json $where");
$categories = array();
while ($category = mysqli_fetch_array($result)){
$my_id = $category['id'];
$category['children'] = OrgChart::getCategories("parent = $my_id");
$categories[] = $category;
}
// mysql_free_result($result);
return $categories;
}
}
Didn't hear back from anyone so I kept exploring line by line. To solve, here is what I found:
In my code, return self::getCategories('parent = 0'); was the culprit. The use of "0" or the use of "= NULL" yielded no results. I'm sure this is well known (but not to me which is why I asked for help) but you must use "IS NULL" or "IS NOT NULL" when working with NULL values for this type of query. I post this in case there are others - like me - who don't currently know this.
I have a bunch of photos on a page and using jQuery UI's Sortable plugin, to allow for them to be reordered.
When my sortable function fires, it writes a new order sequence:
1030:0,1031:1,1032:2,1040:3,1033:4
Each item of the comma delimited string, consists of the photo ID and the order position, separated by a colon. When the user has completely finished their reordering, I'm posting this order sequence to a PHP page via AJAX, to store the changes in the database. Here's where I get into trouble.
I have no problem getting my script to work, but I'm pretty sure it's the incorrect way to achieve what I want, and will suffer hugely in performance and resources - I'm hoping somebody could advise me as to what would be the best approach.
This is my PHP script that deals with the sequence:
if ($sorted_order) {
$exploded_order = explode(',',$sorted_order);
foreach ($exploded_order as $order_part) {
$exploded_part = explode(':',$order_part);
$part_count = 0;
foreach ($exploded_part as $part) {
$part_count++;
if ($part_count == 1) {
$photo_id = $part;
} elseif ($part_count == 2) {
$order = $part;
}
$SQL = "UPDATE article_photos ";
$SQL .= "SET order_pos = :order_pos ";
$SQL .= "WHERE photo_id = :photo_id;";
... rest of PDO stuff ...
}
}
}
My concerns arise from the nested foreach functions and also running so many database updates. If a given sequence contained 150 items, would this script cry for help? If it will, how could I improve it?
** This is for an admin page, so it won't be heavily abused **
you can use one update, with some cleaver code like so:
create the array $data['order'] in the loop then:
$q = "UPDATE article_photos SET order_pos = (CASE photo_id ";
foreach($data['order'] as $sort => $id){
$q .= " WHEN {$id} THEN {$sort}";
}
$q .= " END ) WHERE photo_id IN (".implode(",",$data['order']).")";
a little clearer perhaps
UPDATE article_photos SET order_pos = (CASE photo_id
WHEN id = 1 THEN 999
WHEN id = 2 THEN 1000
WHEN id = 3 THEN 1001
END)
WHERE photo_id IN (1,2,3)
i use this approach for exactly what your doing, updating sort orders
No need for the second foreach: you know it's going to be two parts if your data passes validation (I'm assuming you validated this. If not: you should =) so just do:
if (count($exploded_part) == 2) {
$id = $exploded_part[0];
$seq = $exploded_part[1];
/* rest of code */
} else {
/* error - data does not conform despite validation */
}
As for update hammering: do your DB updates in a transaction. Your db will queue the ops, but not commit them to the main DB until you commit the transaction, at which point it'll happily do the update "for real" at lightning speed.
I suggest making your script even simplier and changing names of the variables, so the code would be way more readable.
$parts = explode(',',$sorted_order);
foreach ($parts as $part) {
list($id, $position) = explode(':',$order_part);
//Now you can work with $id and $position ;
}
More info about list: http://php.net/manual/en/function.list.php
Also, about performance and your data structure:
The way you store your data is not perfect. But that way you will not suffer any performance issues, that way you need to send less data, less overhead overall.
However the drawback of your data structure is that most probably you will be unable to establish relationships between tables and make joins or alter table structure in a correct way.
I'm sure my inability to solve this problem steams from a lack of knowledge of some aspect of php but I've been trying to solve it for a month now with no luck. Here is a simplified version of the problem.
In my database I have a members table, a childrens table (the children of each member), and a friend requests table (this contains the friend requests children send to each other).
What I'm attempting to do is display the children of a particular parent using the following while loop....
$query = "SELECT * From children " . <br>
"WHERE parent_member_id = $member_id"; <br>
$result = mysql_query($query) <br>
or die(mysql_error());<br>
$num_children = mysql_num_rows($result);<br>
echo $num_children;<br>
while($row = mysql_fetch_array($result)){<br>
$first_name = $row['first_name'];<br>
$child_id = $row['child_id'];<br>
<div>echo $first_name<br>
}
This while loop works perfectly and displays something like this...
1) Kenneth
2) Larry
What I'm attempting to do though is also display the number of friend requests each child has next to their name...like this
Kenneth (2)
Larry (5)
To do this I attempted the following modification to my original while loop...
$query = "SELECT * From children " .<br>
"WHERE parent_member_id = $member_id";<br>
$result = mysql_query($query) <br>
or die(mysql_error());<br>
$num_movies = mysql_num_rows($result);<br>
echo $num_movies;<br>
while($row = mysql_fetch_array($result)){<br>
$first_name = $row['first_name'];<br>
$child_id = $row['child_id'];<br>
echo $first_name; include('counting_friend_requests.php') ;
}
In this version the included script looks like this...
$query = "SELECT <br>children.age,children.child_id,children.functioning_level,children.gender,children.parent_member_id,children.photo, children.first_name,friend_requests.request_id " .
"FROM children, friend_requests " .
"WHERE children.child_id = friend_requests.friend_two " .
"AND friend_requests.friend_one = $child_id"; <br>
$result = mysql_query($query)<br>
or die(mysql_error());<br>
$count = mysql_num_rows($result);<br>
if ($count==0)<br>
{<br>
$color = "";<br>
}<br>
else<br>
{<br>
$color = "red";<br>
}<br>
echo span style='color:$color' ;<br>
echo $count;<br>
echo /span;<br>
Again this while loop begins to work but the included file causes the loop to stop after the first record is returned and produces the following output...
Kenneth (2)
So my question is, is there a way to display my desired results without interrupting
the while loop? I'd appreciate it if anyone could even point me in the right direction!!
Avoid performing sub queries in code like the plague, because it will drag your database engine down as the number of records increase; think <members> + 1 queries.
You can create the query like so to directly get the result you need (untested):
SELECT child_id, first_name, COUNT(friend_two) AS nr_of_requests
From children
LEFT JOIN friend_requests ON friend_one = child_id OR friend_two = child_id
WHERE parent_member_id = $member_id
GROUP BY child_id, first_name;
It joins the children table records with friend_requests based on either friend column; it then groups based on the child_id to make the count() work.
You don't need to include the php file everytime you loop. Try creating a Person class that has a method getFriendRequestCount(). This method can all the database. This also means you can create methods like getGriendRequests() which could return an array of the friend requests, names etc. Then you could use count($myPerson->getFriendRequests()) to get the number. Thousands of options!
A great place to start, http://php.net/manual/en/language.oop5.php
Another example of a simple class, http://edrackham.com/php/php-class-tutorial/
Eg.
include ('class.Person.php');
while(loop through members)
$p = new Person(member_id)
echo $p->getName()
echo $p->getFriendRequestCount()
foreach($p->getFriendRequests as $fr)
echo $fr['Name']
In your Person class you want to have a constructor that grabs the member from the database and saves it into a private variable. That variable can then be accessed by your functions to proform SQL queries on that member.
Just to clarify whats happening here.
"include" processing is done when the script is parsed. Essentially its just copying the text from the include file into the current file. After this is done the logic is then parsed.
You should keep any include statements separate from you main logic. In most cases the "include"d code will contain definitions for one or more functions. You can then call these functions from the main body of your program at the appropriate place.
This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
Is it possible to query a tree structure table in MySQL in a single query, to any depth?
I have an admin area I created that pulls data from the mysql database using php and display the results in a table. Basically it shows a parent category, then the first sub category below it, then the third level sub category/subject.
It works perfectly but as I am new to mysql and php I am sure that it the code needs to be improved in order to save db resources as while building the table I use 3 while loops and in each loop make a mysql query which I am sure is the wrong way to do it.
Can somebody offer me some assistance for the best way of doing this?
Here is the code:
$query = mysql_query("SELECT * FROM categories WHERE
parent_id is null
order by cat_id asc;", $hd)
or die ("Unable to run query");
while ($row = mysql_fetch_assoc($query)) {
echo '<tr style="font-weight:bold;color:green;"><td>'. $row ['cat_id'].'</td><td>'.$row['cat_name'].'</td><td>'.$row ['parent_id'].'</td><td>'.$row['active'].'</td><td>'.$row ['url'].'</td><td>'.$row['date_updated'].'</td></tr>' ;
$query2 = mysql_query("SELECT * FROM categories WHERE
(active = 'true' AND parent_id = ".$row ['cat_id'].")
order by cat_id asc;", $hd)
or die ("Unable to run query");
while ($row2 = mysql_fetch_assoc($query2)) {
echo '<tr style="font-weight:bold;"><td>'. $row2['cat_id'].'</td><td>'.$row2 ['cat_name'].'</td><td>'.$row2['parent_id'].'</td><td>'.$row2 ['active'].'</td><td>'.$row2['url'].'</td><td>'.$row2 ['date_updated'].'</td></tr>' ;
$query3 = mysql_query("SELECT * FROM categories WHERE
(active = 'true' AND parent_id = ".$row2 ['cat_id'].")
order by cat_id asc;", $hd)
or die ("Unable to run query");
while ($row3 = mysql_fetch_assoc($query3)) {
echo '<tr><td>'. $row3['cat_id'].'</td><td>'.$row3['cat_name'].'</td><td>'.$row3 ['parent_id'].'</td><td>'.$row3['active'].'</td><td>'.$row3 ['url'].'</td><td>'.$row3['date_updated'].'</td></tr>' ;
}
}
}
EDIT
Ok so I did a bit of research and this is where I am:
Probably for a small database my approach is fine.
For a bigger database using an array to store the data would probably mean I need to use a recursive approach which might use up too much memory. Would love to hear what people think, would it still be better than looping db queries in the nested while loops?
I found the following thread where there is an answer to do this without reccursion and with only one query. Not sure if I need to add a position column to my current design:
How to build unlimited level of menu through PHP and mysql
If I rebuild the design using the nested sets model instead of adjacency model then the mysql query would return the results in the required order however maintaining the nested sets design is above my head and I think would be overkill.
That's it. If anyone has any input on top of that please add to the conversation. There must be a winning approach as this kind of requirement must be needed for loads of web applications.
I would think you could do something like this:
SELECT * FROM categories
WHERE active = 'true'
ORDER BY parent_id, cat_id
This would give you all your categories ordered by parent_id, then by cat_id. You would then take the result set and build a multi-dimensional array from it. You could then loop through this array much as you currently do in order to output the categories.
While this is better from a DB access standpoint, it would also consume more memory as you need to keep this larger array in memory. So it really is a trade-off that you need to consider.
There is a lot to fix there, but I'll just address your question about reducing queries. I suggest getting rid of the WHERE clauses all together and use if statements within the while loop. Use external variables to hold all the results that match a particular condition, then echo them all at once after the loop. Something like this (I put a bunch of your stuff in variables for brevity)
//before loop
$firstInfoSet = '';
$secondInfoSet = '';
$thirdInfoSet = '';
//in while loop
if($parentID == NULL)
{
$firstInfoSet.= $yourFirstLineOfHtml;
}
if($active && $parentID == $catID) // good for query 2 and 3 as they are identical
{
$secondInfoSet.= $yourSecondLineOfHtml;
$thirdInfoSet.= $yourThirdLineOfHtml;
}
//after loop
echo $firstInfoSet . $secondInfoSet . $thirdInfoSet;
You can now make whatever kinds of groupings you want, easily modify them if need be, and put the results wherever you want.
--EDIT--
After better understanding the question...
$query = mysql_query("SELECT * FROM categories order by cat_id asc;", $hd);
$while ($row = mysql_fetch_assoc($query)){
if($row['parent_id'] == NULL){
//echo out your desired html from your first query
}
if($row['active'] && $row['parent_id']== $row['cat_id']){
//echo out your desired html from your 2nd and 3rd queries
}
}
I want to print a individual comment in drupal based on it's comment ID. How can I do this? Google and other sources have yielded me nothing. Thank you.
Eaton's suggestion is good (except it's {comments}, not {comment}) if you need to display the comment like core does it, including the info coming from the node. Except the default theme_comment implementation in modules/comment/comment.tpl.php makes no use of $node.
However, I'd do it slightly differently, because if you need to extract a single comment, displaying it with the normal content formatting provided by comment.tpl.php is likely to be inappropriate.
function print_comment($cid) {
$sql = "SELECT * FROM {comment} c WHERE c.cid = %d";
if ($comment = db_fetch_object(db_rewrite_sql(db_query($sql, $cid), 'c'))) {
return theme('my_special_comment_formatting', $comment);
}
}
And of course, define this special commment formatting in your module's hook_theme() implementation, inspired by what comment.tpl.php does.
2014-02 UPDATE: note that this is a 2009 question/answer. In Drupal 8, you just don't want to access the hypothetical underlying SQL database (and would not do it like this anyway, but use DBTNG), but just use something like:
if ($comment = entity_load('comment', $cid)) {
return entity_view($comment, $view_mode);
}
function print_comment($cid) {
$sql = "SELECT * FROM {comments} WHERE cid = %d";
if ($comment = db_fetch_object(db_query($sql, $cid))) {
$node = node_load($comment->nid);
return theme('comment', $comment, $node);
}
}
No reason to use any sql to do this, two drupal api function calls is all it takes.
function print_comment($cid)
{
$comment = _comment_load($cid);
return theme('comment',$comment);
}