A series of questions from the IRC #RubyOnRails channel on Freenode prompted this example about doing nested, self-referential models.
- Self-referential Models
- Category Tree
- Designing the Model and the database table.
- Extending a class with “nice” methods.
- You promised me a sister!
- Ensuring there is only one root node
- Ensuring that root stays root
- Changing the root
- Siblings, redux
- The class code:
For my example (and the example the OP was working on), the model was categories, which could have parent categories, child categories and sibling categories.
In the figure above, a tree of categories show the concept of parents, children, and siblings. “Category Root” has no parent, and is the root of the tree. “Category A” has one parent, “Category Root”, 2 siblings “Category B” and “Category C”, and 2 children, “Category A.1” and “Category A.2”.
Designing the Model and the database table.
Representing this in a Rails model gets interesting.
We need some way of saying a category has a parent. This is a
foreign key that points back to the category’s table’s
It turns out this is the only specific addition we’ll need to make to the table, and rest will be done by the Model class.
Keeping it simple, we’ll call that field
parent_id following Rails’s
naming conventions for putting the
_id on the foreign key and the
name of the relationship as the foreign key.
Here’s what the migration might look like:
(N.B.: you need to create the foreign key after the table it’s
referencing has been created, so make sure the foreign key definition
is outsite the
Turning to the model now, we have to put in the directives that tell Rails how to use this self-referecing foreign key.
This is all we need to be able to be able to link up a category with it’s parent, and to be able to find the children of a parent.
When the parent is nil, the category has no parent. As of yet, there is nothing to prevent many root nodes as nothing forces a category node to have a parent.
Let’s try a few of these out:
We can see that “A.1”’s parent is “A”, and the “ROOT” has no parent at all, as we expect.
Extending a class with “nice” methods.
It’s easy enough to use something like
to find out if the current category is the root of the tree. We could
make it a bit more clear though by adding in some predicate methods on
Aside: what’s up with that bang-bang?
!! is the double negative, which ensures that whatever result of the
expression, it always evaluates to
false only, i.e.,
nil, and not some other value. This is somewhat of a debate;
some people absolutely hate the
!! because it’s a bit jarring,
others want to be absolutely sure no other values leak through. Leave it
off if it bothers you.
Besides parent, you might want to know if the node is not a
parent. You can always say
!category.parent?, but there’s a solitary
bang operator again waiting to be missed. If we use another method
here we can obtain more readability.
You promised me a sister!
What about siblings? This also turns out to be fairly easy.
This gives all the children of the current node’s parent without itself. In addition, there is the special case where the current node has no parent, so we get all the other nodes without parents.
Ensuring there is only one root node
Earlier I made mention that this model so far doesn’t guarantee there will only ever be one root of the category tree. It is perfectly okay to have multiple root nodes if that fits your need. In this particular instance I only want one root.
To do this, we will ensure that any new category added to the class will always have it’s parent set to the root node, unless the parent is passed in. But what about setting the very first category where there are no other nodes? It turns out this is rather simple to accomodate as well without doing a lot of conditionals.
This looks at the current object
self and sees if the parent id is
presently nil. If it is, it then searches for the current root of the
class and returns it. The “magic” happens when there is no root,
i.e. the first one in the class,
self.class.root returns nil,
setting the parent ID to
nil, just what we want.
Subsequent saves will make sure that the category will at least point to the root.
Ensuring that root stays root
“But wait!” you say, “what happens if I set the root’s parent to a specific category id? What happens then, mousegirl?”
It’s true, with that method
ensure_one_root up there, you can lose
the root of the tree if there is no root. Let’s add a guard and stop
the save if that’s tried:
The guard clause will find out if the current object
self is the
same as the root by fetching the root via the class method. Returning
false from the callback ends the save operation.
Changing the root
Sometimes you do want to change the root. For this, we will need to create an atomic operation because we need to change two values simultaneiously, and step around the before save callback, which is some trickiness.
First we will make a class method that will do the actual swap. In this sort of operation, we’d probably be as likely to want to use the class method as the instance method, so we’ll define it in one place. My personal preference is to put these sorts of things up in the class, but there’s no hard-and-fast rule about it, to my knowledge. (Please comment if you find otherwise?)
update_column (which calls
in order to bypass the
before_save callback. (reference)
Next we’ll add an instance method that just calls the class method:
And we can see it works:
For a single-root system, we don’t need the special case of
siblings, but it turns out it works anyway, returning an empty
relationship because there can be no other top-level ndoes. I think
it’s a better solution to return an empty relationship in this case as
it unifies the expected return with a non-root node with no
siblings. (For example, Category C.1 in the diagram at the
The class code:
Here’s our final code:
You can see this application with tests at the Github Repository.