tags:

views:

460

answers:

6

Hey,

I've been thinking about this one for quite some time now, I need a way to add replies to comments in the database but I'm not sure how to proceed.

This is my currently comment table (doesn't say much but its a start):

CREATE TABLE IF NOT EXISTS `comments` (
  `id` int(12) NOT NULL AUTO_INCREMENT,
  `comment` text,
  `user_id` int(12) DEFAULT NULL,
  `topic_id` int(12) NOT NULL,
  `ts` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `user_id` (`user_id`),
  KEY `topic_id` (`topic_id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 AUTO_INCREMENT=27 ;

and here is my current query:

SELECT c.id, c.comment, c.user_id, u.username, u.photo
FROM (comments c)
JOIN users u ON c.user_id = u.id
WHERE c.topic_id = 9

One option would be to create a new table called "comment_replies" but I'm not sure If I'm able to select all the comments and comment replies in one query, and If I add a new column called "reply" I'm not sure how to sort them to get each comment with each reply.

I would love to get some advice on how to deal with this.

Edit:

Following the below answers about adding parent_comment_id result in this kind of array from 1 comment and 2 replies:

array(2) {
  [0]=>
  object(stdClass)#17 (7) {
    ["id"]=>
    string(2) "26"
    ["comment"]=>
    string(36) "adding a comment from the admin page"
    ["user_id"]=>
    string(2) "16"
    ["ts"]=>
    string(10) "1249869350"
    ["username"]=>
    string(5) "Admin"
    ["photo"]=>
    string(13) "gravatar2.png"
    ["reply"]=>
    string(23) "There is no admin page!"
  }
  [1]=>
  object(stdClass)#18 (7) {
    ["id"]=>
    string(2) "26"
    ["comment"]=>
    string(36) "adding a comment from the admin page"
    ["user_id"]=>
    string(2) "16"
    ["ts"]=>
    string(10) "1249869350"
    ["username"]=>
    string(5) "Admin"
    ["photo"]=>
    string(13) "gravatar2.png"
    ["reply"]=>
    string(13) "Yes there is!"
  }
}

How should I process this array to work with it, Is it possible to separate the comment from the replies?

A: 

A comment reply is a comment with a parent comment_id. Try adding comment_id as a field to your comments table. What you will get is a tree-like structure.

If you wish to retrieve an entire tree of comments, your best bet is to use a nested set (https://wiki.htc.lan/Hierarchy_model). But that's a more complicated solution.

Here's some more info from MySQL: http://dev.mysql.com/tech-resources/articles/hierarchical-data.html

Dimitry Z
+1  A: 

Add a parent_comment_id column to your comments table. Make it optional. When you query, you can join all child comments to each parent. As a bit of selective denormalization (slight redundancy) you can make sure topic_id is set on the child comments as well, letting you pull them all a bit easier (assuming you're going to display all child comments in the main comment thread and not via smaller ajax requests).

Build the presentation however you need, toss the results into memcached (or a flat file, or memory... however you're caching) and you're set.

Toby Joe Boudreaux
+1  A: 

If you want people to be able to reply to the replies (i.e. have a hierarchy of replies such as you would see in, say, an online message forum), then I would add an optional parent_comment_id field to the comments table.

Your table would look like this

`CREATE TABLE IF NOT EXISTS `comments` (
  `id` int(12) NOT NULL AUTO_INCREMENT,
  `parent_comment_id` int(12) NULL,
  `comment` text,
  `user_id` int(12) DEFAULT NULL,
  `topic_id` int(12) NOT NULL,
  `ts` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `user_id` (`user_id`),
  KEY `topic_id` (`topic_id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 AUTO_INCREMENT=27 ;`

Your query showing all comments and replies would be something like:

SELECT c.id, c.comment, r.comment as reply, c.user_id, u.username, u.photo
FROM (comments c)
JOIN users u ON c.user_id = u.id
LEFT JOIN comments r ON c.id = r.parent_comment_id
WHERE c.topic_id = 9

Note however that with this query your replies would also show up not only in the 'reply' column, but also in the 'comment' column as additional rows each with zero or more replies.

To show the username of the users who replied to a comment, you will need to join twice to the users table (first for the user who posted the original comment, and again for the user(s) who replied). Try this query to show the usernames of the users who replied:

SELECT c.id, c.comment, c.user_id, u.username, u.photo, r.comment as reply, r.user_id as reply_user_id, 
u2.username as reply_username, u2.photo as reply_photo
FROM (comment c)
JOIN users u ON c.user_id = u.id
LEFT JOIN comments r ON c.id = r.parent_comment_id
JOIN users u2 ON r.user_id = u2.id
WHERE c.topic_id = 9
flayto
I added the parent_comment_id and changed the query but I'm not sure what to do next. Lets say I have 1 comment with 2 replies, this would generate 2 comments, both will have the same comment but different replies and both replies will have the first comments username. I edited my main post with an example.
Dennis
flayto
Looks good, but it would still output a duplicated comment value where you have more then one reply for one comment, do I have to alter the result array with server-side code to separate the comments from the replies?
Dennis
A: 

Looks like you are working with WordPress, adding a parent_comment_id would have been an ideal solution, but not in this case.

Firstly, I don't think modifying the WordPress basic tables is a good idea. Secondly, you'll end-up with a complex code that will break with wordpress updates.

Best is use a plugin like Intense Comments

Still if you want to create your own solution, I would say create another table for comment replies. a) Your new table would look like this

`CREATE TABLE IF NOT EXISTS `comment_replies` (
  `id` int(12) NOT NULL AUTO_INCREMENT,
  `parent_comment_id` int(12) NULL,
  `comment` text,
  `user_id` int(12) DEFAULT NULL,
  `topic_id` int(12) NOT NULL,
  `ts` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `user_id` (`user_id`),
  KEY `topic_id` (`topic_id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8

b) You'd fetch them this way

$comment_ids = array_map( 'intval', array_keys( $comments ) );
sort( $comment_ids );
$comments_id_list = join( ',', $comment_ids );

$query = "SELECT c.id, c.comment, c.user_id, u.username, u.photo
FROM (comment_replies c)
JOIN users u ON c.user_id = u.id
WHERE c.parent_comment_id IN ( $comments_id_list )"

$replies = $wpdb->get_results($query);
$replies_by_parent = array();

foreach ( array_keys( $replies ) as $i ) {    
    $replies_by_parent [$replies[$i]->id] = array();
}
foreach ( array_keys( $replies ) as $i ) {    
    $replies_by_parent [$replies[$i]->id][] =& $replies[$i];
}

c) Now within your comments loop you can get the replies like this

foreach (  $replies_by_parent [$parent_id] as $reply ) {      
        echo $reply->comment;
    }
Arpit Tambi
I don't use wordpress, but the solution looks quite good. I have to look more into it to see if its optimal for me.
Dennis
A: 

I decided to add the parent_id column in the database and instead of left joining the replies I just selected all the comments at once to later on sort the comments and replies with server-side code, heres the query:

SELECT c.*, u.username, u.photo
FROM (comments c)
JOIN users u ON c.user_id = u.id
WHERE c.topic_id = 9
ORDER BY c.id ASC

Now I pass the query result to the below function so that every reply will be added as an array inside the comment array, so basically it returns a multidimensional array.

function sort_comments($ar)
{
    $comments = array();
    foreach($ar as $item)
    {
     if(is_null($item['parent_id'])) $comments[] = $item;
     else 
     {
      $parent_array = array_search_key($item['parent_id'],$comments,'id');
      if($parent_array !== false) $comments[$parent_array]['replies'][] = $item;
     }
    }
    return $comments;
}
Dennis
A: 

this seems all good but what about if the table contained over a million rows? and some comments could be hundreds of thousands of rows apart. how will these queries perform?

Thats true, but its very unlikely right now, I think the best way would be to separate them to two different tables if you have that much data to handle.
Dennis