Using Proc objects with fragment caching
Caching a highly dynamic application can be hard. Page caching doesn’t work well because some elements of the page need to change where others don’t. Action caching helps in the very few instances where you want all of your before filters to run before loading the cached view. In this instance the before filters are typically redundant and may be doing some heavy lifting that can really slow things down. Fragment caching will cache a certain section of the view or layout but seems somewhat useless at first glance because all of the heavy lifting was already done in the controller.
How do you get around this problem? There are really only three options:
1) Put the necessary code into a model and call it in the view inside a fragment cache. This essentially by-passes the controller and completely breaks MVC rules.
2) Put the necessary code into a helper and call it in the view inside a fragment cache. This works in situations where a helper is necessary, but usually this isn’t what we’re looking for. Calling heavy lifting that’s in helpers from a view is not quite as ugly as calling from a model, but is generally bad design. Helpers should only manipulate existing data, not pull new data.
3) Pass a Proc object from the controller to the view, and call it from inside the fragment cache.
The third option is best. It doesn’t break MVC because the code that gets executed is only executed inside the controller argument. It’s execution is simply delayed until it gets called from within the view. It does dirty your views somewhat and probably won’t work for liquid templates, but it (mostly) follows the rules and gets the job done in a way that is more elegant than the first two options.
This is how you do it.
In this instance, I want to retrieve data that is used for the ‘category_links’ action and view (normally called as a partial). Notice that the Event model contains the actual code to retrieve the data. Recent trends in how to separate model and controller functionality have convinced me this is the way to go. Also note that the controller still lies between the view in model in this setup. See: skinny-controller-fat-model
def category_links
# Set up a Ruby Proc method to pass (usually to a view). This
# method is defined here but cannot be called until @get_event_categories.call
# is used. This allows the logic execution to be deferred until view
# time, but it is defined here (where it should be). This doesn't
# break MVC rules while maintaining a clean syntax. See Ruby Proc Class
@get_event_categories = lambda do
categories = Event.categories.collect {|c| [c.category, c.event_count, {
:controller => 'events',
:action => 'list',
'event[category]' => c.category,
'event_date[start_date]' => date_range_start,
'event_date[end_date]' => date_range_end}]}
sub_categories = Event.sub_categories.collect {|c| [c.sub_category, c.event_count, {
:controller => 'events',
:action => 'list',
'event[sub_category]' => c.sub_category,
'event_date[start_date]' => date_range_start,
'event_date[end_date]' => date_range_end}]}
return categories, sub_categories
end
end@get_event_categories is defined as an object proc variable that contains the code needed to retrieve the categories and sub_categories variables. None of the code was actually executed. Ruby has just stored the code and the environment needed to run it in @get_event_categories.
For completeness sake, here is what gets called above via the Event API:
def categories
conditions = Event.theme_filters
find_hash = {}
find_hash[:select] = 'events.id,category,count(events.id) AS event_count'
find_hash[:conditions] = conditions if conditions && conditions != ''
find_hash[:group] = 'category'
find(:all, find_hash)
end
def sub_categories
conditions = Event.theme_filters
conditions += ' AND ' if conditions && conditions != ''
find(:all,
:select => 'id,sub_category, count(events.id) AS event_count',
:conditions => "#{conditions} sub_category != ''",
:group => 'sub_category')
endHere is the view code for category_links:
<% categories, sub_categories = @get_event_categories.call%>
<% categories.each do |category, event_count, cat_params| %>
<%= link_to_remote category, {
:url => cat_params,
:loading => "Element.show('status')"}, {
:href => url_for(cat_params)} %>
(<%= event_count %>)
<% end %>
<% sub_categories.each do |sub_category, event_count, sub_cat_params| %>
<%= link_to_remote sub_category, {
:url => sub_cat_params,
:loading => "Element.show('status')"}, {
:href => url_for(sub_cat_params)}%>
(<%= event_count %>)
<% end %>The @get_event_categories proc method is called first and the result is copied into categories, and sub_catagories variables. These are then used to generate the actual links.
No caching you say? Since these are almost always called as partials, I don’t cache here. It is assumed that a direct call wants a non-cached version.
My event layout contains the following rail section:
<% cache(:action_suffix => "rail_links") do %><%= render :partial => 'price_links', :layout => false %>
Browse by date
<%= render :partial => 'date_links', :layout => false %>
Browse by category
<%= render :partial => 'category_links', :layout => false %>
Browse by location
<%= render :partial => 'location_links', :layout => false %>
Browse by venue
<%= render :partial => 'venue_links', :layout => false %>
<% end %>
Notice that the entire rail gets cached, and the category links (generated with our proc call) are among them. All of the other links have similar proc calls. I have a before_filter in my controller that retrieves the proc objects before going to the view (for the appropriate actions). If the cache is hit, then the proc methods are never called and no time is wasted on the expensive data pulls and calculations. You may notice that the category link calls are fairly expensive (they group and count the results, which adds a lot of time to the query). The other links are just as expensive. Doing this literally saves seconds on the page load speed.
So there it is. Try it out and marvel at the newfound speed of your application!
Trackbacks
Use the following link to trackback from your own site:
http://blog.chrispcritter.com/trackbacks?article_id=26
My brain hurts from looking at that … will definitely have to read it over and take it a little slower! ;)