Ruby on Rails: Polymorphic Associations with Mixin Modules

by Charles Max Wood on July 31, 2009

I work for a lead generation company. Part of the application I’m working required us to know how many people viewed a particular item in the system. So, my co-worker and I created an Impression model that tracked each appearance of the associated object. We created a polymorphic association to allow impressions to be gathered on several different models.

The trick was that we found ourselves writing the same functionality on each of the models that needed impressions gathered. So, we took that functionality as well as the has_many polymorphic association and moved them into a module. Here’s what the module looks like:

module ImpressionableMixin
  def self.included(base)
    base.instance_eval("has_many :impressions, :as => :impressionable")
  end

  def add_impression(visitor)
    Impression.create(:visitor => visitor, :impressionable => self)
  end
end
 

This bit of code does two things for us. First, it creates the has many association on the model with the ImpressionableMixin module is included. Second, it adds the functionality related to the association to the model, which allows us to keep our code DRY. In other words, our models look like this:

class SomeModel < ActiveRecord::Base
  # associations ...

  include ImpressionableMixin

  # methods ...
end

I’ve left the module’s file in /app/models as it gets loaded when the models are loaded. This allows Rails to load it up when it loads all of the models.

I really like the encapsulation it provides for the functionality and the clean interface it provides.

  • http://skanev.com Stefan Kanev

    You don’t need the instance eval. You can simply do base.has_many

    I tend to end up doing this every time I use a polymorphic relationship

  • drool

    This is how DataMapper suggests to handle polymorphic relationships within it (as opposed to using STI).

  • http://oxos.pl Wojciech

    add_impression isn’t necessary, as ActiveRecord’s association proxies provide us with some methods like:

    some_record.impressions.create :visitor=>visitor

    - no need to pass :impressionable=>self

    Of course you can wrap this in a custom method for abstraction:

    def impression( visitor )
    self.impressions.create :visitor=>visitor
    end

    But I recommend against creating such abstractions until it’s necessary – there’s a good chance ActiveRecord associations will do. You can even extend them, take a look at “association extensions”:
    http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html

  • Pingback: Rails Metal Example #7: Tracking Analytics

  • Pingback: http://charlesmaxwood.com/ruby-on-rails-… « bst On Web Dev

  • Pingback: How to add a Ruby on Rails association from inside a module/mixin « The Missing Readme

  • http://sites.google.com/site/alikhajeh1 Ali

    Very useful, thanks for sharing, I would suggest using a block rather than a string:

    def self.included(base)
    base.instance_eval { has_many :impressions, :as => :impressionable }
    end

Previous post:

Next post: