15
votes

I have a Book and Download model that share many attributes, so my goal is to inherit the common attributes from a DownloadableResource model.
Had a look at STI, but I went the abstract base model class way instead:

  • models:

    class DownloadableResource < ActiveRecord::Base
      self.abstract_class = true
    
      attr_accessible :title, :url, :description, :active, :position
      validates :title, :url, :description, presence: true
      scope :active, where(active: true).order(:position)
    end
    
    class Book < DownloadableResource
      attr_accessible :cover_url, :authors
      validates :cover_url, :authors, presence: true
    end
    
    class Download < DownloadableResource
      attr_accessible :icon_url
      validates :icon_url, presence: true
    end
    
  • migrations:

    class CreateDownloadableResources < ActiveRecord::Migration
      def change
        create_table :downloadable_resources do |t|
          t.string    :title
          t.string    :url
          t.text      :description
          t.boolean   :active,      default: false
          t.integer   :position
          t.timestamps
        end
      end
    end
    
    class CreateBooks < ActiveRecord::Migration
      def change
        create_table :books do |t|
          t.string :cover_url
          t.string :authors
          t.timestamps
        end
      end
    end
    
    class CreateDownloads < ActiveRecord::Migration
      def change
        create_table :downloads do |t|
          t.string :icon_url
          t.timestamps
        end
      end
    end
    

After migration, when I create a new Book the result is far from expected:

> Book.new
=> #<Book id: nil, cover_url: nil, authors: nil, created_at: nil, updated_at: nil> 

Can somebody please shed some light on how to implement the Abstract Base Model Class technique so ActiveRecord models can share common code via inheritance yet be persisted to different database tables?

2
One way is a composition instead of inheritance. Some example: rails-bestpractices.com/posts/17-extract-into-moduleVadimAlekseev
As a side note, even if you go with two similarly structured tables, you can at least keep your migrations DRY by first creating all tables with only their unique fields and then doing something like [:books, :downloads].each do |table| change_table table do |t| t.text :description # ... end endJanosch

2 Answers

13
votes

By declaring a model as abstract you are actually saying that there's no underlying table and you want to allow subclassing. That means:

  • You don't need the downloadable_resources table
  • Book.table_name prints books instead of downloadable_resources

As @Finbarr already mentioned, this also means that both Book and Download models need to have all of the attributes in their tables.

What is this actually useful for then? In my opinion not for much. You can share validations, scopes etc. but you can achieve all of that more easily by including custom modules.

To solve your problem I would probably go with a different approach. I would create another model called DownloadableContent that would be self contained. It would include validations and the table would have all of the attributes. And finally models Book and Download would have a polymorphic has_one relation to the DownloadableContent model.

You could go with the STI approach but I generally don't like mixing all of the custom attributes together.

5
votes

There shouldn't be a downloadable_resources table in this case. Both your books and downloads tables should declare all of the fields they need.