Nested data with Mahana Hierarchy library

Note from Jeff:

Like PDO? Check out how to use it with Codeigniter 
Codeigniter with PDO Thanks!

Nested comments are a nice way to display the flow of a conversation, but figuring out how to pull that data from the tables for display without writing horrible recursive hits on your database is a pain. Nested sets are usually the best solution, but have their pros & cons. I'd like to show you a different technique that works quite well for things like blog comments, using a little CodeIgniter library I threw together.

The idea behind this style is to track the "lineage" of each comment - its parent, grand-parent, etc - all in one field. Then sort on that field. That's it. Like falling off a cliff.

So, a record 3 levels deep might look like:


id name article_id parent_id lineage deep
1 Parent 1 1 0 00001 0
2 Child 1 1 1 00001-00002 1
3 Child 1 a 1 2 00001-00002-00003 2

The full technique is explained here: http://ferdychristant.com/blog//archive/DOMM-7QJPM7 I'm only going to explain how to use my library to work with the data in this way.

First, grab yourself a copy of the library from github or composer and pop it into your libraries directory. It's all one file, no messy set up.

To make this technique work, your table needs to have a primary key, a parent_id, and two fields we'll call "lineage" and "deep", although you can configure those to be what you want. (This is in addition to your actual comment field, the link to the outside table (article_id), and any other data you want to track)

Notice variables at the top of the library:

//set this to whatever is most useful for you
protected $db = 'default';
protected $table = 'hierarchy';
//if you rename your table fields, also rename them here
protected $primary_key = 'id';
protected $parent_id = 'parent_id';
protected $lineage = 'lineage';
protected $deep = 'deep';

This allows you to use any set of names to wish - just make sure they match the functionality. So if you have an existing table with parent_id's you can "snap it" right onto to your table.

IMPORTANT NOTE!: I have added the ability to use a $config and initialize() function so that you can use multiple instances of the library. This works like most CodeIgniter libraries - $config is an array, the keys of which are the variables listed directly above. If you use two configurations of the library in the same page, set up the second one by calling:

$config = array('table'=>'hierarchy');
$this->load->library('mahana_hierarchy');
$this->mahana_hierarchy->initialize($config); 

$config = array('table'=>'products');
$this->mahana_hierarchy->initialize($config);

You can pick which variables need overridden - you don't need to set them all

If you are adding this to an existing dataset, the next thing you want to do is update all your table rows to have a lineage and deep value, by running:

$this->mahana_hierarchy->resync();

This will read over all your table rows and set the appropriate values.

And now...you are ready to use it!

So, to insert data, we need to feed an array of the normal record data - i.e., comment body, create date, etc. If you include a parent id it will build from that, otherwise it will assume this is a new top-level record. No need to do anything with lineage or deep - the library will calculate all of that for you.

Example:

//add a top-level parent

$new_comment['name'] = "A new parent record";

$insert_id = $this->mahana_hierarchy->insert($new_comment);

// add a child

$new_comment['name'] = "A new parent record";

$new_comment['parent_id'] = $insert_id;

$this->mahana_hierarchy->insert($new_comment);

If we want to display this data,



$data = $this->mahana_hierarchy->get();         
var_dump($data);

(this example is showing the where clause usage - it assumes you've been building up your data and have an article_id = 5)

$data = $this->mahana_hierarchy->where(array('article_id '=>5))->get();         
var_dump($data);

The get() function returns all the records from a certain parent (optional) down. We can use the where() function as a chainable method that simply uses the CodeIgniter's Active record class where(). If we needed to start at a specific point in the hierarchy, pass that record's id as a parameter in get();

Additional functions include:

get_one($id)

Returns a single record

get_children($parent_id)

Returns all direct children of the parent, but no other descendents

get_descendents($parent_id)

Returns all the descendents of the given parent (but not the parent)

get_grouped_children($top_id=0)

Fetch all descendent records based on the parent id, ordered by their lineage, and groups them as a mulit-dimensional array.

get_ancestors($id, $remove_this = false)

Returns an array of all the ancestors of the given record. Second parameter will leave out the requesting record or not

get_parent($id)

Returns an row array of the parent of the given record.

insert($data)

Inserts new record, as explained above

update($id, $data)

This is a simple Active Record update, and will not resync all your lineages! Run resync if you need to do that

delete($id, $with_children=false)

Delete a record. It will NOT delete its children as well unless you tell it to

max_deep()

Just lets you know how deep your child records are getting

Hope that proves useful - as always, interested in comments & code pull requests



Comments

  • Noobigniter

    2012-10-24

    Thank you :)

  • Bogdan

    2013-02-05

    Thanks,
    Nice and simple !

  • Deep Purple

    2013-03-22

    Thank you for nice and simple library. I miss one or two methods for reordering lineage. For example if need to insert record at any position in the tree? Is there a semple solution. Thanks in advance.

  • Jeff Madsen

    2013-03-22

    @Deep Purple - thanks!

    To insert, just give the parent_id as part of the insert array, then run $this->mahana_hierarchy->resync(); to update hierarchy

    Unfortunately, with this or any nested set technique, you'll always have to resynch afterwards if you insert above a particular node

  • Luke

    2013-06-06

    Hello,
    I just found Your library. Looks like a great solution. Good work, thanks. But I have a question. I would like to somehow combine this with Jamie Rumbelow's MY_Model. For similarities with the rest of my scripts, and to utilize the benefits of MY_Model as created_ad, validation, protected atributes etc... Do You have any idea how to do it easily and correctly? Thanks in advance.

  • Jeff Madsen

    2013-06-07

    Hi Luke,

    I guess that would work without too much trouble - notice that Jamie uses $_db & $_table, slightly different. You would be completely overwriting the basic CRUD statements, as they are a bit different & very specific in how they have to work.

    Guess you'd just have to sit down and hack through it - feel free to drop me a line if you get stuck on anything

  • Nigel

    2013-06-10

    hi Jeff. I like this very much. I cannot seem to get the get_grouped_children($top_id=0) function to return anything.

    I had assumed that if get_children($id) works so would this. delving a bit into your code, I see that nothing is returned from the recursive function _findchildren()

  • Luke

    2013-06-11

    Hi Jeff,
    Thank You for Your reply. I did it like this: I created Hierarchy_model that extends MY_Model and placed in the method from Your library. I converted them so that they used the CRUD methods from MY_Model. Most changes require the delete, it added support for soft_delete and trigger events. In addition, I added a method that supports hierarchy get_dropdown ... for example, to select from a list of categories, with the exception of the current element, for example, when you edit this category. Now I can load this Hierarchy_model into another model, which requires the use hierarchy.
    I have a question: why in the lineage You use 00001 instead of simply 1? I guess this has some reasons?

    With Regards

  • Jeff Madsen

    2013-06-13

    @Luke - as per our emails, the padding is to sort the lineage as a string, rather than numbers, to keep in proper order

  • Jeff Madsen

    2013-06-13

    @Nigel - as per our emails, if your parent_id field defaults to 0, change the _findChildren() to default the same way

  • Scott

    2013-06-27

    Luke - it would be neat if you could share your implementation with My_Model as I'm in a similar boat as you. (Or if Jeff will be so kind to hook me up with your email so I can contact you directly)

    Anyway, great blog Jeff.

  • Subha Sundar Das

    2013-06-28

    I m developing a Yellowpages Business directory there I need to create multi dimentional category and subcategory. I found your CI Librrary very usefull....Its help to save my Time..

    thanks

  • Jeff Madsen

    2013-06-28

    Great to hear! Glad it helped

  • Deepak Thomas

    2013-10-20

    Firstly, thanks a TON for this! Works amazing, and you've saved me hours of coding.

    For threads with 100s of comments, it would be great to have a 'limit' parameter and pagination support.

  • Jeff Madsen

    2013-10-20

    Thanks Deepak,

    Yes, not a bad idea. I'm probably going to shift my focus to a framework agnostic version soon, but it is an easy thing to extend if you want to have a go. Drop me a line if you need any help

  • Deepak Thomas

    2013-10-20

    Great, Jeff! Looking forward to it. Meanwhile, I'll try some quick mods for limit/pagination.

Post a comment