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.rbclass Thing < ApplicationRecord has_one_attached :imageend
# app/controllers/things_controller.rbclass ThingsController < ApplicationController def index @things = Thing.all endend
# 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: :blobend
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.rbclass ThingsController < ApplicationController def index @things = Thing.with_attached_image.all endend
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.where.missing(:image_attachment)
Here, we piggybacked off the implementation of the earlier scope we saw, with_image_attachment.
Throw this behind a scope and you’re cooking!
Follow me on Twitter.