depends_on

written by stonean on May 27th, 2008 @ 09:20 AM

For one of my projects I had the need for a polymorphic association in which one object required the existence of the other. I had written a specific version of this, but decided it could easily be abstracted. I didn't go with the built in ActiveRecord polymorphic features as I wanted to strip it down to the basics to see if it would work. Why? I would love to use this in DataMapper at some point.


module Stonean
  module DependsOn
    def self.included(base)
      base.extend Stonean::DependsOn::ClassMethods
    end

    module ClassMethods
      def depends_on(model_sym, options = {}) 
        has_one model_sym, 
                :foreign_key => "#{options[:as]}_id",
                :conditions => "#{options[:as]}_type = '#{self.name}'"

        validates_presence_of model_sym
        validates_associated model_sym

        define_save_method(model_sym, options[:as])
        before_save "save_#{model_sym}".to_sym

        options[:attrs].each{|attr| define_accessors(model_sym, attr)}
      end
      
      def define_save_method(model_sym, poly)
        define_method "save_#{model_sym}" do
          eval("self.#{model_sym}.#{poly}_type = self.class.name")
          eval("self.#{model_sym}.#{poly}_id = self.id")
          eval("self.#{model_sym}.save")
        end
      end

      def define_accessors(model_sym, attr)
        define_method attr do
          eval("self.#{model_sym} ? self.#{model_sym}.#{attr} : nil")
        end

        define_method "#{attr}=" do |val|
          model_defined = eval("self.#{model_sym}")

          unless model_defined
           eval("self.#{model_sym} = self.build_#{model_sym}")
          end

          eval("self.#{model_sym}.#{attr}= val")
        end
      end

    end
  end
end
ActiveRecord::Base.send :include, Stonean::DependsOn

Here's an example, but first a little information. Content is a model that all "presentable" objects in my cms depend on. It holds the name and url attributes so when you are calling message.name, you are really calling message.content.name.

class Message < ActiveRecord::Base
  depends_on :content, :attrs => [:name, :url], :as => :presentable
end

This means your form and views can use these methods and not worry about the underlying association. for example:

 text_field_tag "message[name]", value

Now you don't have to do anything special in your views or controller to build and save the depends_on object.

This example is very specific, but with a little modification, can be used to implement a class table inheritance architecture. I definitely plan on doing this soon.

So what are the drawbacks? The major one is the find method. As it's written now, you will make two calls to the database: one for the main object and one for the dependent object. That blows. I will be addressing this issue very soon.

I will be releasing this as a gem in the near future, but for now you can just copy the code above and add it into your config/initializers directory.

If you have any ideas or suggestions, I would love to hear them.

Comments are closed