Search code examples
ruby-on-railsactiverecordrails-activestorage

How to add an activestorage attachment without triggering activerecord callbacks


In my app, I have a large complex model with many associations. I need to serve it as json. The model's size and complexity, however, mean that assembling the json version is costly both in terms of CPU and RAM. Fortunately, since the model is not frequently updated it's a good candidate for caching. I'd like to keep a cached version of the model's json string on a static file hosting service (e.g. S3), handled by activestorage.

The code below summarizes what's I've done so far. In the controller, incoming GET requests for the model are redirected to the cached version on activestorage. The model keeps the cached version up-to-date using an after_save callback.

class ComplicatedModelController < ApplicationController
  def show
    @complicated_model = ComplicatedModel.find(params[:id])
    redirect_to @complicated_model.activestorage_cached_json_response.url
  end
class ComplicatedModel < ActiveRecord::Base

  has_one_attached :activestorage_cached_json_response

  after_save do 
    # updated cached version - how can I do this WITHOUT retriggering the aftersave callback?
    self.activestorage_cached_json_response.attach(
      io: StringIO.new(self.as_json),    # this step is costly
      filename: "ComplicatedModel_#{self.id}.json",
      content_type: 'application/json'
    ) 
  end
end

The problem is that updating the activestorage attachment within the aftersave callback creates an infinite loop: this step saves the model again, and so retriggers the callback. To make this strategy work, I'd need to update the activestorage attachment WITHOUT triggering the aftersave callback. Is this possible?


Solution

  • Ah, I figured it out. I just needed to use an :unless after the callback, as is explained here: https://stackoverflow.com/a/7386222/765287

    Working code is below.

    class ComplicatedModel < ActiveRecord::Base
      attr_accessor :skip_aftersave_callback
    
      has_one_attached :activestorage_cached_json_response 
    
    
      after_save :update_cache, unless: :skip_aftersave_callback  # change here
    
      def update_cache
    
        # update_cache is always called in the after_save callback;    
        #   this flag stops the callback after the .attach below
        self.skip_aftersave_callback = true
    
        self.activestorage_cached_json_response.attach(
          io: StringIO.new(self.as_json),    # this step is costly
          filename: "ComplicatedModel_#{self.id}.json",
          content_type: 'application/json'
        ) 
      end
    end