Rant: BetterNestedSet (than a nail in your skull)

I’ve been tinkering around with ApeDoctor, my to-be API documentation application running on Rails. The core class of ApeDoctor is the Amodule, a representation of a Ruby module/class. (It’s Amodule because the class name Module was taken, of course!) And since Ruby modules can have other modules or classes under them, I had a hierarchy/tree/nested set on my hands.

My first instinct was to use acts_as_tree. Ah, simple, naïve AAT. The logical choice for a beginning Rails developer, and it worked quite well for my humble purposes. Vague warnings on the web of AAT inefficiency didn’t bother me much; I anticipate having fewer than 20 nodes in the tree, and most of the branches quite shallow.

But then a friend recommended BetterNestedSet. Wow, BetterNestedSet! It has the word “Better” right in the name, so it must be really good, right?

Well, after struggling for days to get BNS to meet my simple needs, I find myself asking, “Better than what, someone driving a nail into your skull with a hammer?” I’ve found it extremely awkward to use; I’ve had to jump through more hoops than a show dog, and my best result is barely acceptable.

First of all, you can’t set parent_id, like you do with acts_as_tree. If you try, BNS will bark at you. Instead, you have to use the move_to_child_of method so than BNS can work its behind-the-scenes nested set voodoo.

That doesn’t sound so bad. Maybe change a few lines of code, and it’s working, right? Well, there’s a catch: you can only call move_to_child_of after the record has been created in the database.

Oh. Hmm. Okay, maybe we’ll just use an after_save callback to call it. Aha, Catch 22: you have to store the id of the parent object somewhere, so that you can retrieve it in the callback, but you can’t put it in parent_id, even temporarily, because BNS forbids it.

So maybe we can store it in another instance variable, maybe name it @new_parent or such, and then use that in the callback. That’s workable. You’ll have to go through some hoops in your view and controller to support it. The lovely scaffold you generated, with a field for setting parent_id, is broken by BNS, but we can just change that to set new_parent instead…

Except that passing a hash of attributes (like the params in your controller’s create action, made with the form values) will only set attributes that map directly onto a database column. So Rails won’t set our @new_parent attribute from the params. But you can fake it by doing:

@amodule = Amodule.new(params[:amodule])
@amodule.new_parent = params[:amodule][:new_parent]

Okay, we’re getting somewhere! It seems to work, everything is going great…

Unless you want to, say, do validation involving the parent. In my case, I wanted to make sure that sibling modules had unique names, but not be fussy about having the same name as a module under a different parent. The natural way to express that would be:

validates_uniqueness_of :name, :scope => :parent_id

But that won’t work with BNS, because parent_id won’t be set at validation time – it only gets set after the record has been saved. And you can’t change the scope to use :new_parent, our temporary attribute, because that’s not what the database column is called. So you’re left to write your own makeshift uniqueness validation:

def validate
    same = Amodule.find(:all,
                       :conditions => {
                         :name => self.name,
                         :parent_id => @new_parent
                       })

    same.each do |mod|
      if( mod == self )
        errors.add(:name, "already exists under this parent")
      end
    end
end

Congratulations, you now have an awkward, makeshift, hackish nested set model. Wasn’t that so much better than using acts_as_tree? At least it’ll be nice and efficient… well, except for inserting, moving, or deleting nodes, which require practically the entire table to be rewritten.

Conclusion: BetterNestedSet was a major pain in the ass, is completely unnecessary for my modest purposes, wasted days of my life, and above all soured my feelings towards Rails and this project. But I’m sure the friend that suggested it meant well, so I won’t drive a nail into his skull to thank him.

</rant>

This entry was posted in Projects, Ruby and tagged , , , . Bookmark the permalink. Post a comment or leave a trackback: Trackback URL.

Post a Comment

Your email is never published nor shared. Required fields are marked *

*
*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>