Active Storage N+1 Queries with has_one_attached and has_many_attached

author-avatar

One of the greatest additions to recent versions of Ruby On Rails is the introduction of Active Storage, which helps us manage file uploads for different storage services and handles Active Record associations and migrations out of the box. A common method from the Active Storage module is has_one_attached, used to establish a one to one mapping between a model and a file (a user’s avatar in our example):

class User < ApplicationRecord
  has_one_attached: :avatar, dependent: :destroy
end

We do not need to define the avatar column, Active Storage will handle this under the hood by creating a has_one association to a ActiveStorage::Attachment record and a has_one: through: association to an ActiveStorage::Blob record. There is no need to access these associations directly, we can just call the association name to get a user’s avatar:

user = User.last
user.avatar

A very common feature to implement in an `index` action would be to display a list of resources and their respective avatar:

<% @users.each do |user| %>
  <% if user.avatar.attached? %>
    <%= image_tag(user.avatar(resize_to_limit: [100, 100])) %>
  <% else %>
    <%= image_tag "placeholder.png" %>
  <% end %>
<% end %>

This example will cause an N+1 query since there is a one to one mapping between our model (User) and its ActiveStorage::Attachment (avatar). The solution to this is to eager load users’ attachments, just like you would do for any Active Record association. Active Storage provides the with_attached_x method to accomplish this:

<% @users.with_attached_avatar.each do |user| %>
  <% if user.avatar.attached? %>
    <%= image_tag(user.avatar(resize_to_limit: [100, 100])) %>
   <% else %>
     <%= image_tag "placeholder.png" %>
   <% end %>
 <% end %>

It is possible to set up a one-to-many relationship between models and files using the `has_many_attached` macro:

class User < ApplicationRecord
  has_many_attached: :images, dependent: :destroy
end

N+1 problems related to Active Storage can be handled using the with_attached_attachments helper:

<% @users.with_attached_images.each do |user| %>
  <% if user.images.attached? %>
    <% user.images.each do |image| %>
      <%= image_tag(image(resize_to_limit: [100, 100])) %>
    <% end %>
  <% end %>
<% end %>

The result is that loading images will take just one query, instead of nested database queries occurring within a loop.