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.
-
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 endThen, once the ActiveRecord callbacks are called, sweepers are able to call expire_fragment.
Marston A. : Maurycy, Thanks for the example and snippet, I'll give this a shot. Is this something I can just stick in a model?Steve Madsen : This is a great trick, and I wouldn't call it a hack. You're setting up just enough state for the sweeper to do its job and letting everything else work as it normally does. -
Disclaimer: My rails is a bit rusty, but this or something like it should work
ActionController::Base.new.expire_fragment(key, options = nil)Orion Edwards : Did anyone try this? I don't have rails handy but I'm pretty sure it will solve the problem...Cimm : I tried this in Rails 3.0.0.rc and it works great, thanks! It doesn't remove the folder hierarchy in the /tmp/cache directory but it does remove the cache file and that's what it needs to do. -
I'm a bit of a rails noob, so this may not be correct, or even helpful, but it seems wrong to be trying to call controller actions from within the model.
Is it not possible to write an action within the controller that does what you want and then invoke the controller action from within your rake task?
-
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
-
This might not work for what you're doing, but you may be able to define a custom call back on your model:
class SomeModel < ActiveRecord::Base define_callback :after_exploded def explode ... do something that invalidates your cache ... callback :after_exploded end endYou can then use a sweeper like you would normally:
class SomeModelSweeper < ActionController::Caching::Sweeper observe SomeModel def after_exploded(model) ... expire your cache end endLet me know if this is useful!
-
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.rbor, much preferably, to a new plugin (e.g. createvendor/plugins/controller_from_model/init.rbcontaining 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 endThen, 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? fiAnyhow, in your particular case you might want to inject code into your various
ActiveRecord::Basedescendants when the relevant controller classes load, so that the actual controller-aware code still resides inapp/controllers/*.rb, but it is not mandatory to do so in order to get something functional (though ugly and hard to maintain.)Have fun!
Cheers, V.
titaniumdecoy : Thanks for this! In order to get it to work, I had to put the code into environment.rb (it did not work in a plugin) and change @@allow_concurrency to ActionController::Base.allow_concurrency (which I hope is the same thing).Orion Edwards : Let the foot-shooting commence! -
Will it not be easier and clean just to pass the current controller as an argument to the model method call? Like following:
def delete_cascade(controller)
self.categories.each do |c| c.delete_cascade(controller) controller.expire_fragment(%r{article_manager/list/#{c.id}.*}) end PtSection.delete(self.id) controller.expire_fragment(%r{category_manager/list/#{self.id}.*})end
You can access all public methods and properties of the controller from within model. As long as you do not modify the state of the controller, it should be fine.
-
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.rbclass ActiveRecord::Base def expire_fragment(*args) ActionController::Base.new.expire_fragment(*args) end endNow, you can use expire_fragment on all instances of ActiveRecord::Base, e.g.
User.first.expire_fragment('user-stats')
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.