before_create gotcha with BetterNestedSet plugin

I’m using BetterNestedSet at work in my Rails work but I’ve been having trouble understanding why my code was giving me an error of “Impossible move, target node cannot be inside moved tree”.

I’m trying to create Categories, scoped to certain CategoryTypes. Pretty standard associations.

Categories belong_to CategoryTypes
CategoryTypes has_many Categories

The one thing I’m doing a little different from the BNS readme is that I’m not assigning my scope column id directly. I’m using one of Jamis Buck’s recommendations.

Moving associated creations to the model

I have a “data” attribute subset of fields in my form. It’s just one field “category_type”. Note, not category_type_id. It’s just a choice I made. This gives me the following params being passed to the controller…

{ :text => 'Tom Servo', :data => { :category_type => 1 } }

Then in a before_create method “assign_additional_data” where I look up the CategoryType using the passed id. This is so that it’s possible to raise an error if that CategoryType has been deleted in the short interval between form creation and submission.

before_create :assign_additional_data
...
def assign_additional_data
  return if @data.nil?
  self.category_type = CategoryType.find(@data[:category_type])
end

This is a long way of saying that I have a before_create call being added to the queue of callbacks. However, BNS has one as well. It sets the lft and rgt column values. I was getting caught by the order that the before_create methods were queued up. My model assigned the acts_as_nested_set before my before_create call

class Category < ActiveRecord::Base
  acts_as_nested_set :scope=> :category_type
  before_create :assign_additional_data
  ...
end

I had a clean db table and in my console I created two categories…

>> c1 = Category.create({ :text => 'Satellite of Love', :data => { :category_type => 1 } } )
=> #<Category id: 1, parent_id: nil, lft: 1, rgt: 2, text: "Satellite of Love", category_type_id: 1>
>> c2 = Category.create({ :text => 'Croooow', :data => { :category_type => 1 } } )
=> #<Category id: 1, parent_id: nil, lft: 1, rgt: 2, text: "Croooow", category_type_id: 1>

WHA? Why didn’t the lft and rgt columns increment like they’re supposed to. c2 is supposed to be “3” and “4” respectively, until that category is assigned as a child of another category. I finally (after my headbashing) figured it out. It was because I was assigning the category_type scope AFTER BNS calls it’s before_create method used to assign lft/rgt column values.

Snipped from better_nested_set.rb

before_create :set_left_right
  ...
  # On creation, automatically add the new node to the right of all existing nodes in this tree.
  def set_left_right # already protected by a transaction within #create
    maxright = base_set_class.maximum(right_col_name, :conditions => scope_condition) || 0
    self[left_col_name] = maxright+1
    self[right_col_name] = maxright+2
  end

Because my scope condition parameters weren’t being assigned until after BNS’ before_create method, the scope id that needed to be set wasn’t being set. So my sql statement called for the “maximum” part in the code above came out as…

SELECT max(rgt) AS max_rgt FROM `categories` WHERE (categories.category_type_id IS NULL)

See, category_type_id is NULL, not the “1” I wanted. And because of that maxright gets assigned “0” and the lft and rgt columns are give “1” and “2”, respectively.

Well, let’s reverse the order in my Category model and see what happens.

class Category < ActiveRecord::Base
  before_create :assign_additional_data
  acts_as_nested_set :scope=> :category_type
  ...
end

Creating the two categories again gives us this…

:text => 'Tom Servo', :lft => 1, :rgt => 2, :category_type_id => 1
:text => 'Croooow', :lft => 3, :rgt => 4, :category_type => 1

Excellent! It worked. Let’s see what the “maximum” sql statement says…

SELECT max(rgt) AS max_rgt FROM `categories` WHERE (categories.category_type_id = 1)

Ahh, finally. category_type_id is being assigned the correct value.

So, if you are getting the “Impossible move, target node cannot be inside moved tree” error, check to see that if you’re using your own before_create methods, that they’re assigned before your call to acts_as_nested_set.