How to call expire_fragment from Rails Observer/Model?

13,762

Solution 1

Disclaimer: My rails is a bit rusty, but this or something like it should work

ActionController::Base.new.expire_fragment(key, options = nil) 

Solution 2

The solution provided by Orion works perfectly. As an enhancement and for convenience, I've put the following code into config/initializers/active_record_expire_fragment.rb

class ActiveRecord::Base
  def expire_fragment(*args)
    ActionController::Base.new.expire_fragment(*args)
  end
end

Now, you can use expire_fragment on all instances of ActiveRecord::Base, e.g. User.first.expire_fragment('user-stats')

Solution 3

This is quite easy to do. You can implement Orion's suggestion, but you can also implement the broader technique illustrated below, which gives you access to the current controller from any model and for whichever purpose you decided to break MVC separation for (e.g. messing with the fragment cache, accessing current_user, generating paths/URLs, etc.)

In order to gain access to the current request's controller (if any) from any model, add the following to environment.rb or, much preferably, to a new plugin (e.g. create vendor/plugins/controller_from_model/init.rb containing the code below):

module ActiveRecord
  class Base
    protected
      def self.thread_safe_current_controller #:nodoc:
        Thread.current[:current_controller]
      end

      def self.thread_safe_current_controller=(controller) #:nodoc:
        Thread.current[:current_controller] = controller
      end

      # pick up the correct current_controller version
      #  from @@allow_concurrency
      if @@allow_concurrency
        alias_method :current_controller,  :thread_safe_current_controller
        alias_method :current_controller=, :thread_safe_current_controller=
      else
        cattr_accessor :current_controller
      end
  end
end

Then, in app/controllers/application.rb,

class ApplicationController < ActionController::Base
  before_filter { |controller|
    # all models in this thread/process refer to this controller
    #  while processing this request
    ActiveRecord::Base.current_controller = controller
  }

  ...

Then, from any model,

if controller = ActiveRecord::Base.current_controller
  # called from within a user request
else
  # no controller is available, didn't get here from a request - maybe irb?
fi

Anyhow, in your particular case you might want to inject code into your various ActiveRecord::Base descendants when the relevant controller classes load, so that the actual controller-aware code still resides in app/controllers/*.rb, but it is not mandatory to do so in order to get something functional (though ugly and hard to maintain.)

Have fun!

Solution 4

In one of my scripts I use the following hack:

  require 'action_controller/test_process'

  sweepers = [ApartmentSweeper]

  ActiveRecord::Base.observers = sweepers
  ActiveRecord::Base.instantiate_observers

  controller = ActionController::Base.new
  controller.request = ActionController::TestRequest.new
  controller.instance_eval do
    @url = ActionController::UrlRewriter.new(request, {})
  end

  sweepers.each do |sweeper|
    sweeper.instance.controller = controller
  end

Then, once the ActiveRecord callbacks are called, sweepers are able to call expire_fragment.

Solution 5

Why not have your external rake tasks call the expiry method on the controller. Then you're still being MVC compliant, you aren't building in a dependence on some scoping hack, etc.

For that matter, why don't you just put all the daemon / external functionality on a controller and have rake / cron just call that. It would be loads easier to maintain.

-- MarkusQ

Share:
13,762
Marston A.
Author by

Marston A.

Updated on June 17, 2022

Comments

  • Marston A.
    Marston A. about 2 years

    I've pretty much tried everything, but it seems impossible to use expire_fragment from models? I know you're not supposed to and it's non-MVC, but surely there much be some way to do it.

    I created a module in lib/cache_helper.rb with all my expire helpers, within each are just a bunch of expire_fragment calls. I have all my cache sweepers setup under /app/sweepers and have an "include CacheHelper" in my application controller so expiring cache within the app when called via controllers works fine.

    Then things is I have some external daemons and especially some recurring cron tasks which call a rake task that calls a certain method. This method does some processing and inputs entries into the model, after which I need to expire cache.

    What's the best way to do this as I can't specify cache sweeper within the model. Straight up observers seem to be the best solution but then it complains about expire_fragment being undefined etc etc, I've even tried including the ActionController caching classes into the observer but that didn't work. I'd love some ideas of how to create a solution for this. Thanks.