has_cache: cache no Rails de maneira simples - Nando Vieira
Há algum tempo atrás mostrei como utilizar as opções de cache disponíveis no Ruby on Rails 2.1.Embora esta tarefa tenha se tornado mais simples, ainda exige um processo um tanto quanto manual.
Pensando nisso, comecei a estudar formas mais automáticas de se fazer isso. Eu já simpatizava com a idéia implementada pelo Geoffrey Grosenbach, onde ele de mostrou uma forma muito inteligente de lidar com cache ou, melhor dizendo, com sua expiração.
A idéia consiste basicamente em utilizar uma data de atualização do objeto para manter o controle de cache. Desta forma, sempre que a data for atualizada, o cache irá expirar automaticamente. Esta abordagem é especialmente útil quando utilizada com o Memcache, já que ele irá utilizar uma quantidade pré-definida de memória, descartando os itens mais antigos quando ela se esgotar.
A solução que encontrei para este problema foi empacotada na forma de um plugin: has_cache.
Utilizando o plugin has_cache
Para usar este plugin, basta você instalá-lo através do comando abaixo:
script/plugin install git://github.com/fnando/has_cache.git
Vale lembrar que este plugin exige as versões 2.1 ou superior do Ruby on Rails.
O plugin possui funcionalidades específicas para models, actions e views, como você verá a seguir.
Depois de instalado, basta adicionar a seguinte chamada ao seu modelo:
has_many :publishers
has_cache
def recent_comments
comments.recent :limit => 5
Apenas por adicionar a chamada ao método has_cache, todas as associações has_many e belongs_to irão ter uma versão com cache. Por exemplo, em vez de utilizar @game.comments, você pode utilizar @game.cached_comments. Simples assim!
Você também pode adicionar métodos que não são relacionamentos; basta utilizar a opção :include.
has_cache :include => :recent_comments
O exemplo acima irá adicionar o método de instância recent_comments. Se precisar adicionar métodos de classe, pode adicionar uma chamada como esta:
has_cache :include => {
:class_methods => %w(all find),
:instance_methods => :recent_comments
Os métodos de classe possuem um argumento obrigatório, que é a chave que irá identificar aquele cache.
Game.cached_all :sorted_by_title, :order => 'title asc'
Se você instalar o plugin has_paginate, poderá fazer consultas paginadas com cache.
Game.cached_paginate %w(all @page), :order => 'title asc', :page => @page
@game.cached_comments(:page => @page)
Se quiser evitar a paginação, basta passar a opção :paginate com o valor false.
@game.cached_comments(:paginate => false)
ATENÇÃO: Se o plugin has_paginate estiver instalado, todas as associações has_many serão paginadas por padrão.
Mais à frente você verá como o cache é expirado, e como definir novas chaves que serão expiradas.
No controller, você pode utilizar o método cached_render:
@page = [params[:page].to_i, 1].max
cached_render :cache_name => %w(games index#{@page}) do
@games = Game.cached_paginate %w(all #{@page}), @page
Você pode especificar o tempo de vida do cache com a opção :expires_in.
cached_render :expires_in => 15.minutes do
Sempre que puder, utilize esta abordagem. Porém, se algum detalhe da tela é diferente para os usuários — um usuário logado tem um box com alguma identificação —, você não conseguirá fazer cache de toda a action. Mas poderá ter uma boa performance fazendo cache de pedaços da tela.
Você pode fazer cache de fragmentos de um template. O plugin has_cache adiciona um método chamado cached_block.
<% cached_block [:game_list, @page] do %>
<% each_paginate @games do |game, i| %>
<%= game.title %>
<%= paginate @games, url_for(:action => 'index') %>
Você também pode definir o tempo de vida do cache com a opção :expires_in.
<% cached_block [:game_list, @page], :expires_in => 1.hour do %>
Como funciona a expiração do cache
Todo relacionamento has_many precisa de um campo com o nome do relacionamento, que servirá como o controle de expiração do cache. No nosso exemplo, nosso modelo deveria ser criado da seguinte forma:
class CreateGames < ActiveRecord::Migration
create_table :games do |t|
t.datetime :comments_updated_at, :publishers_updated_at
drop_table :games
Toda vez que um comentário for criado, o plugin irá atualizar automaticamente o campo comments_updated_at, que é utilizado na composição da chave que irá identificar o cache. O mesmo irá acontecer quando um novo publisher for adicionado.
As seguintes estruturas de chave são expiradas quando um objeto é salvo ou destruído:
próprio objeto
:table/:id
:table/:id-:updated_at
associações has_many
:table/:id/:updated_at/:association/:association_updated_at
associações belongs_to
métodos de instância
:table/:id/:updated_at/:method_name
Não são expirados automaticamente.
Como os métodos de classe recebem uma chave na hora que são chamados, não podem ser expirados automaticamente. Neste caso, você pode deixar o método expirar por tempo ou forçar sua expiração.
has_cache :before_expire => proc {|game|
Game.has_cache_options[:to_expire] << "games/sorted_by_title"
Se você tiver alguma sugestão, faça um fork do has_cache e envie um patch. Dúvidas? Mande um comentário.