views:

44

answers:

2

Hi,

I'd like to be able to describe different types of a model using RoR associations. An example:

Models:
Post

ImagePost
post_id:integer
url:string

MessagePost
post_id:integer
message:string

ImagePost and MessagePost are a type of Post. I'd like @posts = Post.all to retrieve both types of post and allow me access to their attributes via @posts.url or @posts.message.

I'm sure I'm missing something simple, please enlighten me!

Cheers,

Ben.

+2  A: 
JacobM
I had read about Single Table Inheritance but was put off due to the redundant fields that result from storing everything in one table. I then went on to look at polymorphic relationships but this didn't seem to fit either. If this is best practice for my scenario thats what I'll go with. Thanks for your help!
Ben
A: 

You could try the mixed model approach, but it's a fair amount of work to set up. It's bit of of a kludge that trades performance for more efficient database storage.

The idea is that you use STI to handle all the common Post fields, and delegate the unique fields for each subclass to an another table and eager load that association.

The base Post class could look like this. Note the class_eval could be abstracted into a module that gets included and exended into a subclass.

#columns: id:integer, timestamps, user_id:integer, 
#  topic_id:integer, type:string
class Post < ActiveRecord::Base
  # common methods/validations/associations
  belongs_to :user
  belongs_to :topic 

  def self.relate_to_detail
    class_eval <<-"EOF"
      has_one :detail, :class_name => "#{self.name}Detail"
      accepts_nested_attributes_for :detail

      default_scope :include => :detail

      def method_missing(method, *args)
        build_detail if detail.nil?
        if detail && detail.respond_to?(method, true)
          detail.send(method, *args)
        else 
          super(method, *args)
        end
      end

      def respond_to?( method, include_private = false)
        build_detail if detail.nil?
        super(method, include_private) || 
          detail.respond_to?(method, include_private)
      end
    EOF
  end
end

Then you'll need to define the sub and detail class for each type.

#uses posts table
class ImagePost < Post
  relate_to_detail
end

#columns: id, image_post_id, url:string, height:integer, :width:integer
class ImagePostDetail < ActiveRecord::Base
  belongs_to :image_post
end

#uses posts table
class MessagePost < Post
  relate_to_detail
end

#columns: id, message_post_id, message:string
class MessagePostDetail < ActiveRecord::Base
  belongs_to :image_post
end

Now we can do things like this:

@user.message_posts.create(:message => "This is a message")
@image_post.url

Which will create a new MessagePost, where the user_id, timestamps, post_id, post_type are all stored in the posts table, while the message is stored in the MessagePostDetails table and return the url of an ImagePost respectively.

The new method_missing and respond_to? definitions work the magic to hide the division.

@Post.all will now list Posts of all types. The only downside is that the detail fields will not be displayed when you fetch a post.

EmFi