ActiveStorage: checking if .attached? for a collection that has attachments preloaded (while avoiding n+1 queries of attached? method)

Viewed 3491

In my Rails 5 application, building a JSON API & using Activestorage for my attachments:

To avoid accessing an attachment that doesn't exist, I am using this if condition as shown in this answer but unfortunately this if condition makes a SQL query to check if the attachment is available or not, causing n+1 queries although I have already included (pre-loaded) the attachments for the records I have

How to check if the attachment exist on a record without hitting the DB, if I have preloaded the attachments already?

I tried to use .nil? but that will give a false positive that there is attachment even if there isn't (as shown below)

ActionView::Template::Error (to_model delegated to attachment, but attachment is nil):
  if !product.logo.nil?
    json.logo_img url_for(product.logo)

.attached? check works but causes the n+1 queries:

  if product.logo.attached?
    json.logo_img url_for(product.logo)

So while the below code looks very ugly, it works, preloading the attachments for all the records at once, but when checking if an attachment exists => this leads to n+1 queries

json.array!(@some_records.includes(integrated_product: [company: [ logo_attachment: :blob ]])) do |one_record|
  product ||= one_record.integrated_product
  if !product.logo.attached?
    json.logo_img url_for(product.logo)
  elsif !product.company.logo.attached?
    json.logo_img url_for(product.company.logo)
  end

....

Any thoughts on how to handle this in a proper way (preloading attachments & avoiding n+1) are appreciated.

Update:

I want to have the whole collection that I passed, including items with & without attachment, searching for a condition that will check if that already loaded item has an attachment or not to display an image if it has attachment

3 Answers

Rails generates a collection method for has_one_attached relationships. You can just do something like this:

Product.all.with_attached_logo.map{|product| product.logo.attached? }

Which will make a single eager loaded query.

Lets say you want to show the logo's of each product inside views/products/index.htm.erb

So let's set up the @products variable in the index action of your controllers/products_controller.rb

def index
  @products = Product.all.with_attached_logo
end

This will eager load the logo attachment into each product if they have one. Now we can create the products index page without the N+1 problem like so:

<% @product.each do |product| %>
  <% if product.logo.attached? %>
    <%= image_tag product.logo %>
  <% else %>
    <%= image_tag "product_logo_placeholder.jpg" %>
  <% end %>
  ... the rest of the product's information ...
<% end %>

The problem turned out to be that the complex includes I had was not actually preloading the attachments (it was preloading for the nested record but not the main one), therefore the .attached? method was making a SQL query to check.

However, when the correct preloading is done, the .attached? works correctly, checking the in-memory object without firing a seperate SQL query to check again

I am leaving the question for the reference of the community if someone face a similar issue.

If I understand you correctly, you might want do do something like this.

In your model:

class Product < ApplicationRecord
  has_one_attached :logo
  scope :with_eager_loaded_logo, -> { eager_load(logo_attachment: :blob) }
end

Then in your controller, for example:

def index
  @products = current_user.products.with_eager_loaded_logo
end

Not a JSON, but still some view example(HTML):

<%= image_tag (product.logo) if product.logo.attached? %>

See more in this post.

Related