Can’t get there from here

In one of my Ruby on Rails projects, I have a model validation which is dependent on attributes from a belongs_to parent. Normally, you can refer to a parent model as child.parent and access the parent’s attributes as child.parent.attr. The difficulty arises when the parent object is unsaved. Let’s look at the code:


class Item < ActiveRecord::Base
  has_many :quantity_limits
  has_many :locations, :through => :quantity_limits
  has_many :line_items
  has_many :orders, :through => :line_items
end

class Location < ActiveRecord::Base
  has_many :orders
  has_many :quantity_limits
  has_many :items, :through => :quantity_limits
end

class QuantityLimit < ActiveRecord::Base
  belongs_to :item
  belongs_to :location

  #Attribute quantity is the max quantity of an item which may be ordered for a location
end

class Order < ActiveRecord::Base
  has_many :line_items, :dependent => :destroy
  belongs_to :location
end

class LineItem < ActiveRecord::Base
  belongs_to :order
  belongs_to :item

  def quantity_limit
    if self.order
      QuantityLimit.find(:first, :conditions => {:location_id => self.order.location_id, :item_id => self.item_id})
    else
      nil
    end
  end

  def validate
    if self.quantity_requested
      if self.quantity_limit
        limit = self.quantity_limit.quantity
      else
        limit = 0
      end

      errors.add(:quantity_requested,"above maximum of " + limit.to_s)if self.quantity_requested > limit
    end
  end
end

The key element is LineItem#quantity_limit, which retrieves the relevant QuantityLimit object. You might expect self.order.location_id to be valid once the LineItem has been added to the Order#line_items collection (order.line_items << new LineItem). If the parent has been saved (i.e. Order#new_record? is false) then LineItem[:order_id] is updated with Order[:id] and everything is copacetic. However, if the parent is unsaved, the child is added to the has_many collection Order#line_items but the LineItem object is not changed. The underlying reason is the has_many collection Order#line_items is more or less an array and doesn’t need a primary key to describe the relationship. The belongs_to reference, on the other hand, stores only the primary key of the parent and can’t be referenced if the primary key hasn’t been established.

OK, I know the smokers are ready for a cigarette after all that, but bear with me. The upshot of all this is a line for a saved Order is properly validated. But if the Order is unsaved, limit is always 0 because self.order is nil and the relevant QuantityLimit cannot be found. But there is a solution…

Ruby to the Rescue

The Ruby core language includes a module called ObjectSpace which allows direct interaction with the garbage collector. Using this, we’re able to determine which Order object contains our LineItem. Let’s look at the new code:


def quantity_limit
  parent_order = self.order

  if !parent_order
    ObjectSpace.each_object(Order) {|o| parent_order = o if o.line_items.include?(self)}
  end

  return nil if !parent_order

  QuantityLimit.find(:first, :conditions => {:location_id => parent_order.location_id, :item_id => self.item_id})
end

This time we’re still checking the normal primary key based reference first, but if it’s nil we take another approach. We iterate through all the live objects of the Order class to find the one that references our LineItem. We’re assuming that only one Order will contain our LineItem, which will be true unless we’ve re-parented a LineItem. Once we’ve located the correct Order, the validation can proceed as before.

While we’re spending a few extra cycles and lines of code to use this method, I think the maintainability benefits of a DRY solution make it the right choice. Please let me know how it works for you!