From Siberia with love

Keeping binary files in database using mongoDB's GridFS

In my side project, I tried to keep all binary files related to it in the database using mongoDB's GridFS. There are numerous posts about that, but what could stop me from writing another one? Main difference from them is the usage of rack-gridfs gem for GETting files.

If you haven't do anything yet you need to create new rails project skipping activerecord:

$ rails new rails-gridfs-demo -O

I decided to use carrierwave as a file uploader and mongoid as mongoDB ODM because they are much better than their analogs for a couple of reasons.

So we need to add them into the project:

Gemfile

gem 'mongoid'
gem 'bson_ext'   , '~> 1.3'
gem 'carrierwave', :git => 'https://github.com/jnicklas/carrierwave.git'
gem 'rack-gridfs', :git => 'https://github.com/skinandbones/rack-gridfs.git' \
                 , :require => 'rack/gridfs'

Next step is to run bundle command, generate mongoid config and configure it as default orm:

$ bundle
$ rails g mongoid:config

config/application.rb

config.generators.orm = :mongoid

Then we are ready to scaffold resource, for example, Image:

$ rails g scaffold Image

To use carrierwave as file uploader it's needed to mount it in the model but before that, we must create and configure it to use gridFS as file storage:

$ rails g uploader Image

app/uploaders/image_uploader.rb

storage :grid_fs

config/initializers/carrierwave.rb

CarrierWave.configure do |config|
  config.grid_fs_database = "rails_gridfs_demo_#{Rails.env}"
  config.grid_fs_host = 'localhost'
  config.grid_fs_access_url = "/uploads"
end

app/models/post.rb

class Image
  include Mongoid::Document

  mount_uploader :image, ImageUploader
end

Then add file field and pass :multipart => true parameter to our form:

app/views/images/_form.html.erb

<%= form_for @image, :html => { :multipart => true } do |f| %>
<..>
<%= f.label :image %>
<%= f.file_field :image %>

And show uploaded image:

app/views/images/show.html.erb

<%= image_tag(@image.image_url) %>

So it seems that everything is configured and ready to run, but if you try to upload an image, it will be uploaded successfully, but won't show up. It happens because carrierwave cares only about how to put your files in GridFS, but not how to get them, so this is why we need rack-gridfs

config/application.rb

config.middleware.insert_after Rack::Runtime, Rack::GridFS,
  :prefix => 'uploads',
  :lookup => :path,
  :database => "rails_gridfs_demo_#{Rails.env}"

From this moment everything should work as expected. Hurray! You can get this demo project from github.

Here is the benchmark using rainbows:

Server Software:
Server Hostname:        0.0.0.0
Server Port:            3000

Document Path:          /uploads/uploads/image/image/4e0a567569a99757d4000001/220px-MVC_schem.png
Document Length:        183 bytes

Concurrency Level:      10
Time taken for tests:   24.966 seconds
Complete requests:      15000
Failed requests:        0
Write errors:           0
Non-2xx responses:      15000
Total transferred:      5325000 bytes
HTML transferred:       2745000 bytes
Requests per second:    600.82 [#/sec] (mean)
Time per request:       16.644 [ms] (mean)
Time per request:       1.664 [ms] (mean, across all concurrent requests)
Transfer rate:          208.29 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0      12
Processing:     2   17   9.9     13      85
Waiting:        2   16   9.9     13      85
Total:          4   17   9.9     13      85

Percentage of the requests served within a certain time (ms)
  50%     13
  66%     13
  75%     14
  80%     14
  90%     40
  95%     41
  98%     44
  99%     50
 100%     85 (longest request)