Pat Shaughnessy

Ribadesella, Spain

Database storage for Paperclip: rewritten to use a single table

April 14, 2009 · 9 comments

Back in February I wrote an implementation of a new storage module for Paperclip that supports saving file attachments in a database table. My original implementation saved the file attachments in a separate database table, which was internally managed using a “has_many” relationship from the target model.

This month I decided to rewrite the code to use a simpler, single table approach. Instead of saving the files in a separate table, each file is saved in a BLOB column in the same table as the target model. This makes the database storage module easier and more intuitive to use, and moves it closer to the original intent of the Paperclip design.

Code: http://github.com/patshaughnessy/paperclip

New usage:

  1. Install and use my version of the plugin:
    script/plugin install git://github.com/patshaughnessy/paperclip.git
  2. In your model specify the "database" storage option; for example:
    has_attached_file :avatar, :storage => :database
    The file will be stored in a column called [attachment name]_file (e.g. "avatar_file") by default. To specify a different BLOB column name, use :column, like this:
    has_attached_file :avatar,
                      :storage => :database,
                      :column => 'avatar_data'
  3. If you have defined different styles, these files will be stored in additional columns called [attachment name]_[style name]_file (e.g. "avatar_thumb_file") by default. To specify different column names for each style, use :column in the style definition, like this:
  4. has_attached_file :avatar,
                      :storage => :database,
                      :styles => { 
                        :medium => {
                          :geometry => "300x300>",
                          :column => 'medium_file'
                        },
                        :thumb => {
                          :geometry => "100x100>",
                          :column => 'thumb_file'
                        }
                      }
  5. You need to create these new columns in your migrations or you'll get an exception. Example:
    add_column :users, :avatar_file, :binary
    add_column :users, :avatar_medium_file, :binary
    add_column :users, :avatar_thumb_file, :binary
    Note the migration for the "binary" column type will not work for LONGBLOBs in MySQL. Here's an example migration for MySQL:
    execute 'ALTER TABLE users ADD COLUMN avatar_file LONGBLOB'
    execute 'ALTER TABLE users ADD COLUMN avatar_medium_file LONGBLOB'
    execute 'ALTER TABLE users ADD COLUMN avatar_thumb_file LONGBLOB'
  6. To avoid performance problems loading all of the BLOB columns every time you access your ActiveRecord object, a class method is provided on your model called “select_without_file_columns_for.” This is set to a hash that will instruct ActiveRecord::Base.find to load all of the columns except the BLOB/file data columns, for example:
    {:select=>"id,name,avatar_file_name,avatar_content_type,..."}
    If you’re using Rails 2.3, you can specify this as a default scope:
    default_scope select_without_file_columns_for(:avatar)
    Or if you’re using Rails 2.1 or 2.2 you can use it to create a named scope:
    named_scope :without_file_data, select_without_file_columns_for(:avatar)
  7. By default, URLs will be set to this pattern:
    /:relative_root/:class/:attachment/:id?style=:style
    Example:
    /app-root-url/users/avatars/23?style=original
    The idea here is that to retrieve a file from the database storage, you will need some controller's code to be executed. Once you pick a controller to use for downloading, you can add this line to generate the download action for the default URL/action (the plural attachment name), "avatars" in this example:
    downloads_files_for :user, :avatar
    Or you can write a download method manually if there are security, logging or other requirements. If you prefer a different URL for downloading files you can specify that in the model; e.g.:
    has_attached_file :avatar,
                      :storage => :database,
                      :url =>'/users/show_avatar/:id/:style'
  8. Add a route for the download to the controller which will handle downloads, if necessary. The default URL, /:relative_root/:class/:attachment/:id?style=:style, will be matched by the default route: :controller/:action/:id

For now, I’ve overwritten my code from February. I believe this implementation is cleaner and easier to use; if anyone did download and use my code from February let me know and I can help you migrate the file data from the separate table back to columns in primary table. I will be writing a script to do this for my own application.

When I have time in the next few days or weeks, I’ll post a sample application that illustrates all of these steps and shows exactly how to do all of this. If anyone has any questions or suggestions please let me know.

Tags:·

9 responses so far ↓

  • 1 Adilson Chacon // Jul 14, 2009 at 05:23 PM

    I'm sorry, but this did not work for me!
  • 2 pat // Jul 20, 2009 at 07:43 AM

    Hi Adilson, Sorry for the slow response; I was travelling last week. I’m sorry to hear you had trouble with this; can you send me an error message or some other technical details about what happened? I’m happy to try to help. If you have time, you could also take a look at the sample app I posted; part 3 has a detailed example on how to use database storage.

  • 3 Cynthia // Oct 13, 2009 at 03:02 PM

    Sorry to see you rewrote this. I agree with your original implementation of having the BLOBs in a separate table. (Didn't your Oracle DBAs complain about the rewrite?) If I branch as of 2009-04-02 I can get the original multiple table version, right?
  • 4 pat // Oct 13, 2009 at 09:13 PM

    Lol... nope the Oracle DBAs didn't seem to care one way or another. I still think having one table is simpler and easier than two. I’m curious to know why you disagree; maybe a separate table allows for more flexibility with Oracle storage/tablespaces or whatever.

    Anyway, yes the old version is in the git history; just take the revision immediately before my “Refactored to save files in columns in the model's database table” commit on April 13. Feel free to fork the repo and post this version in case other people prefer this approach also.

    So:

    git clone git://github.com/patshaughnessy/paperclip.git

    cd paperclip

    git checkout 529ffc4ae337bfeed77a73134072ed2cbf1029aa

  • 5 Oleg // Dec 11, 2009 at 04:07 PM

    Brilliant! Exactly what I was looking for. And single table storage is what I need as well.
  • 6 Victor Costan // May 04, 2010 at 04:16 AM

    Hi, Pat!

    Thank you for the awesome fork! I’ve been using it for a while and it’s been working out really well so far!

    Are you considering pulling the latest paperclip changes, and perhaps getting Rails 3 compatibility? I can do straight uploads with Rails 3 so far, but I can’t seem to get thumbnails generated.

    Thanks! Victor

  • 7 pat // May 04, 2010 at 09:42 AM

    Hi Victor… great to hear the database storage has been useful for you. I’ll definately take a look at using it in Rails 3. I’ve been meaning to get my hands dirty with Rails 3 for a while and this would be the perfect excuse.

    Let me see what I can do and I’ll post a writeup on my blog when I have something.

  • 8 Victor Costan // May 04, 2010 at 09:16 PM

    Hi, Pat!

    I’m running the patched version of your fork on Rails edge, and it works so far. Based on my reading of the code last night (my brain might not have been working), I postulate that it’s sufficient to incorporate thoughtbot’s changes into your fork or graft your changes on top of their new code.

    I’m hoping this comment will help you better estimate the time you need. Once again, thanks so much for your fork!

  • 9 pat // May 31, 2010 at 08:11 AM

    Hi Victor,

    FYI I did this just now: I merged all of the Thoughtbot changes since 2009 into my repo, including their Rails 3 branch.

    If you want to try out database storage on Rails 3 use the “rails3” branch from my repo:

    gem 'paperclip',
      :git => 'git://github.com/patshaughnessy/paperclip.git',
      :branch => 'rails3'


    And you’ll also have to edit application.rb; see my article: How to install Paperclip in a Rails 3 app.

    Thanks for your help! - pat

Leave a Comment