Using belongs_to in Rails model validations when the parent is unsaved

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 =&amp;gt; {:location_id =&amp;gt; parent_order.location_id, :item_id =&amp;gt; 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!

This entry was posted in Ruby on Rails and tagged , . Bookmark the permalink.

11 Responses to Using belongs_to in Rails model validations when the parent is unsaved

  1. Tiffani says:

    Heyy…this was definitely a great blog post! ’twas exactly what I needed when I thought I was really creating something rather screwy.

  2. vint says:

    Thanks, very useful!

  3. Chris says:

    Do you still consider this the best way to do this? I’m in a strikingly similar situation…

  4. Mack Earnhardt says:

    I haven’t needed to revisit this, so I really can’t say.

  5. Peter Boling says:

    Mack,
    Was just googling you and it’s interesting that we are doing the same thing!
    How long have you been working with rails?

    Do you have any open source projects?
    Are still around Indy? Actually I now remember seeing BW3s on your twitter, so I guess so. Have you been to any of the iRug meetings? They’re excellent!

    - Peter Boling

  6. calebhc says:

    Thanks for posting this!

  7. Jens says:

    Hi,

    your method works, but it triggers hundreds (in my case) of superfluous SQL requests due to the “.include?” statement like “SELECT child.id FROM children WHERE (child.id = NULL) AND (child.parent_id = xxx)”.

    Is there a way to avoid these requests?

    Thanks!

  8. Mack Earnhardt says:

    Jen, try this tweak:

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

  9. Jens says:

    Thanks! I’ll try this ASAP. A first experiment seems to work without side effects. -Jens

  10. K-P says:

    Thanks man, you really made my day! I had been googling around for this for quite a while.

  11. Pingback: Validating a polymorphic association for a new record » Rebecca Miller-Webster

Leave a Reply

Your email address will not be published. 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>