Keep your acts_as_attachment models DRY

I think acts_as_attachment is my favourite Rails plugin. Not only does it provide some fundamentally necessary functionality (read: upload and manage files), it does some handy stuff like validate content types, maintain proper parent-children relationships for thumbnails, and comes packaged with a pretty thorough set of test cases. It totally upstages its predecessor, the file_column plugin by Sebastian Kanthak, which isn’t being actively maintained these days anyways.

If there’s one thing I like about Kanthak’s plugin though, and keep in mind this is also its greatest flaw, is that file_column attachments have a minimal impact on your database schema. You just need an extra column for the models you’re attaching files to – totally simple. Acts_as_attachment, on the other hand, wants separate tables for each of your attachment models. Here’s two taken from an imaginary online video store, complete with customer profiles (they’ve jumped on the “community driven” bandwagon):

class DvdCover < ActiveRecord::Base
  belongs_to :dvd # through dvd_id
  acts_as_attachment :content_type => :image
end

class Mugshot < ActiveRecord::Base
  belongs_to :profile # through profile_id
  acts_as_attachment :content_type => :image
end

If you use acts_as_attachment this way, you’ll need two tables named dvd_covers and mugshots. Personally, I’ve got enough tables as it is, and since they’re really both just images, I’d like to store them in one. The solution? Subclass from a generic Image model using the power of Rails’ single table inheritance.

class Image < ActiveRecord::Base
end

class DvdCover < Image
  belongs_to :dvd, :foreign_key => :owner_id
  acts_as_attachment :content_type => :image
End

class Mugshot < Image
  belongs_to :profile, :foreign_key => :owner_id
  acts_as_attachment :content_type => :image
end

Pretty simple huh? Now you’ve got all your images stored in a single ‘images’ table. This may not be for everyone, but I think it’s tidy.

In case you’re wondering, here’s the full database migration for the Image model.

create_table :images do |t|
  t.column "owner_id", :integer   # generic owner
  t.column “type”, :string        # holds the class

  t.column "content_type", :string
  t.column "filename", :string     
  t.column "size", :integer
  t.column "parent_id",  :integer 
  t.column "thumbnail", :string
  t.column "width", :integer  
  t.column "height", :integer
end

There’s just one more thing – we’ve broken the relationship between these image models and their owners. We’ve already fixed up belongs_to to use the owner_id foreign key; we need to do the same for the corresponding has_[one|many] clauses for Dvd and Profile.

class Dvd < ActiveRecord::Base
  has_many :dvd_covers, :foreign_key => :owner_id
  ...
end

class Profile < ActiveRecord::Base
  has_one :mugshot, :foreign_key => :owner_id
  ...
end

Note: Your uploaded DvdCover files will still wind up in /public/dvd_covers, and Mugshots in /public/mugshots.

This technique could be re-used for any number of different attachment types – audio files, video files, etc. For that matter, you could make all your files subclass from a a single generic Attachment model, but that might be overkill.