Easier solution for using BetterNestedSet

More tinkering with BetterNestedSet, and I think I’ve got the solution nailed:

First of all, use redundant columns to store the parent id. One is for your application to access, the other is for BetterNestedSet to handle – I called mine :parent_id and :bns_parent_id. You can tell BNS to use a nonstandard parent column like this:

acts_as_nested_set :parent_column_name => :bns_parent_id

Why two columns to store one piece of data? Well, as I discovered earlier, BetterNestedSet really doesn’t like you playing with its toys, so we’ll just give it a toy of its own. Then use an after_save callback to call BNS’s #move_to_child_of method; this will synchronize :bns_parent_id with the :parent_id that you’ve been setting in your controller, as well as letting BNS do its voodoo nested set magic.

So far, this is pretty much the same solution I had before, except using an extra database column instead of a temporary instance variable. So why is this solution easier than the old one?

Well, for one thing, you get the more natural :parent_id, and all the readability-enhancing niceties that come with it: #parent, #parent=, and so forth (although you’ll have to define those methods in your model yourself). Sure beats coming up with some new variable name (I’d been using :par), especially if your app was already using :parent_id before, e.g. with acts_as_tree.

The second reason is that it makes validation easier. I can now use :parent_id in my validations without any fuss:

validates_uniqueness_of :name,
                        :scope => :parent_id,
                        :message => "is already taken under this parent"

Some caveats: You can’t use certain BNS methods (e.g. ancestors, children, full_set) on new records. The reason for this is that BNS uses its nested set voodoo to find ancestors and children/indirect children, and new records won’t have that special mojo until after they are saved and move_to_child_of is run. Likewise for testing, you will have to save records, and reload their corresponding ActiveRecord objects, when testing relationships.

Now that I have BetterNestedSet tamed, tied up, and gagged, it isn’t so hard to use. Hopefully this post will spare someone out there the frustration that I had to go through!

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")

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.


Music Toy Idea

I had an interesting idea for a musical sound-generation toy, inspired by Electroplankton. The gist of it is that there is a ”sea anemone” on the screen with 5 or so tendrils; at the end of each tendril is a loop that you can grab and pull with the mouse. Each tendril makes a unique sound/pitch, and when you pull a tendril further away from the body, it gets louder. When you let go, it slowly retracts and gets softer. There would be other objects in the scene that you could interact with, such as pins or fishing hooks that would prevent the tendril from retracting.

What would make it even neater is to use Chipmunk to have all of the pieces be physical bodies – a central mass connected by springs to the outer masses.

I wonder how long it would take to create such a toy using Rubygame 3.

Alas, the audio support in Rubygame is woefully inadequate, and messy as well. I don’t want to bog myself down adding even more new features for 3.0.0, but I could restructure the Mixer module enough that it could be expanded in the future without breaking backward compatibility.