Modeling inheritance with Ruby/Rails ORMs

2020-05-01 22:07发布

问题:

I'm trying to model this inheritance for a simple blog system

Blog has many Entries, but they may be different in their nature. I don't want to model the Blog table, my concern is about the entries:

  • simplest entry is an Article that has title and text
  • Quote, however, does not have a title and has short text
  • Media has a url and a comment...
  • etc...

What is a proper way to model this with Ruby on Rails? That is

  • Should I use ActiverRecord for this or switch to DataMapper?
  • I would like to avoid the "one big table" approach with lots of empty cells

When I split the data into Entry + PostData, QuoteData etc can I have belongs_to :entry in these Datas without having has_one ??? in the Entry class? That would be standard way to do it in sql and entry.post_data may be resolved by the entry_id in the postdata table.

EDIT: I don't want to model the Blog table, I can do that, my concern is about the entries and how would the inheritance be mapped to the table(s).

回答1:

I've come across this data problem several times and have tried a few different strategies. I think the one I'm a biggest fan of, is the STI approach as mentioned by cicloon. Make sure you have a type column on your entry table.

class Blog < ActiveRecord::Base
  # this is your generic association that would return all types of entries
  has_many :entries

  # you can also add other associations specific to each type.
  # through STI, rails is aware that a media_entry is in fact an Entry
  # and will do most of the work for you.  These will automatically do what cicloon.
  # did manually via his methods.
  has_many :articles
  has_many :quotes
  has_many :media
end

class Entry < ActiveRecord::Base
end

class Article < Entry 
  has_one :article_data
end

class Quote < Entry
  has_one :quote_data
end

class Media < Entry
  has_one :media_data
end

class ArticleData < ActiveRecord::Base
  belongs_to :article # smart enough to know this is actually an entry
end

class QuoteData < ActiveRecord::Base
  belongs_to :quote
end

class MediaData < ActiveRecord::Base
  belongs_to :media
end

The thing I like about this approach, is you can keep the generic Entry data in the entry model. Abstract out any of the sub-entry type data into their own data tables, and have a has_one association to them, resulting in no extra columns on your entries table. It also works very well for when you're doing your views:

app/views/articles/_article.html.erb
app/views/quotes/_quote.html.erb
app/views/media/_media.html.erb # may be medium here....

and from your views you can do either:

<%= render @blog.entries %> <!-- this will automatically render the appropriate view partial -->

or have more control:

<%= render @blog.quotes %>
<%= render @blog.articles %>

You can find a pretty generic way of generating forms as well, I usually render the generic entry fields in an entries/_form.html.erb partial. Inside that partial, I also have a

<%= form_for @entry do |f| %>
  <%= render :partial => "#{f.object.class.name.tableize}/#{f.object.class.name.underscore}_form", :object => f %>
<% end %> 

type render for the sub form data. The sub forms in turn can use accepts_nested_attributes_for + fields_for to get the data passed through properly.

The only pain I have with this approach, is how to handle the controllers and route helpers. Since each entry is of its own type, you'll either have to create custom controllers / routes for each type (you may want this...) or make a generic one. If you take the generic approach, two things to remember.

1) You can't set a :type field through update attributes, your controller will have to instantiate the appropriate Article.new to save it (you may use a factory here).

2) You'll have to use the becomes() method (@article.becomes(Entry)) to work with the entry as an Entry and not a subclass.

Hope this helps.

Warning, I've actually used Media as a model name in the past. In my case it resulted in a table called medias in rails 2.3.x however in rails 3, it wanted my model to be named Medium and my table media. You may have to add a custom Inflection on this naming, though I'm not sure.



回答2:

You can handle this easily using ActiveRecord STI. It requires you to have a type field in your Entries table. This way you can define your models like this:

def Blog > ActiveRecord::Base
  has_many :entries

  def articles
    entries.where('Type =', 'Article')
  end

  def quotes
    entries.where('Type =', 'Quote')
  end

  def medias
    entries.where('Type =', 'Media')
  end

end

def Entry > ActiveRecord::Base
  belongs_to :blog
end

def Article > Entry
end

def Quote > Entry
end

def Media > Entry
end