ActiveStorage, the new kid on the block. It’s exciting for Ruby on Rails developers to have a built-in solution for file uploads/attachments. Let’s talk about interacting with the table ActiveStorage gives us. There are times that we’ll want to reach for eager loading and even query against our attachments. Without much effort, it’s easy to commit the N+1 sin.

# app/models/thing.rb
class Thing < ApplicationRecord
  has_one_attached :image
end
# app/controllers/things_controller.rb
class ThingsController < ApplicationController
  def index
    @things = Thing.all
  end
end
# app/views/things/index.html.erb
<%= @things.each do |thing| %>
  <%= image_tag rails_blob_url(thing.image) %>
<% end %>

👆This view, my friends, is N+1 city. For every call to an image, ActiveStorage makes a call to a table where the image record resides.

Built-in Eager Loading

Rails, as you might come to expect, has a built-in solution to prevent this. Using the example above, you’ll note that we have a single attached image. Under the hood, this means that a Thing is implicitly setup with the following relationship:

class Thing < ApplicationRecord
  has_one :image_attachment, -> { where(name: 'image') }, class_name: "ActiveStorage::Attachment", as: :record, inverse_of: :record, dependent: false
  has_one :image_blob, through: :image_attachment", class_name: "ActiveStorage::Blob", source: :blob
end

You get all this 👆 for free by adding has_one_attached :image to your model. My attachment is named image, anytime you see a reference to image in my code, change it to your attachment name.

A Rails scope exists to allow us to take advantage of this relationship. If we wanted to preload those images, we could change the controller to use the following scope:

# app/controllers/things_controller.rb
class ThingsController < ApplicationController
  def index
    @things = Thing.with_attached_image.all
  end
end

This scope 👆 uses preloading under the hood 👇

includes(image_attachment: :blob)

Querying Against ActiveStorage Attachments

Preloading is dandy, and all, but what happens when you want to query against the attachment records? This question is similar to the use case that led me here.

Specifically, I only wanted to show records that have an image attached. After digging through the ActiveStorage source code, I ended up finding not only the references to the code above but also the answer to this question.

I’d like to return only the records without an image record. The following is the result 👇

Thing.joins(:image_attachment).where.not(active_storage_attachments: { record_id: nil })

Here, we piggybacked off the implementation of the earlier scope we saw, with_image_attachment.

Throw this behind a scope and you’re cooking!