2009-06-23 8 views
10

Diciamo che si dispone di un frammento della pagina che visualizza i post più recenti e si scade in 30 minuti. Sto usando Rails qui.Il modo migliore per combinare frammenti e memorizzazione nella cache di oggetti per memcached e Rails

<% cache("recent_posts", :expires_in => 30.minutes) do %> 
    ... 
<% end %> 

Ovviamente non c'è bisogno di fare la ricerca nel database per ottenere i post più recenti, se esiste il frammento, così si dovrebbe essere in grado di evitare il carico di troppo.

Quello che sto facendo ora è qualcosa di simile nel controller che sembra funzionare:

unless Rails.cache.exist? "views/recent_posts" 
    @posts = Post.find(:all, :limit=>20, :order=>"updated_at DESC") 
end 

È questo il modo migliore? È sicuro?

Una cosa che non capisco è il motivo per cui la chiave è "recent_posts" per il frammento e "views/recent_posts" al momento del check in seguito, ma mi si avvicinò con questo dopo aver visto memcached -vv per vedere che cosa stava usando. Inoltre, non mi piace la duplicazione di inserire manualmente "recent_posts", sarebbe meglio tenerlo in un posto.

Idee?

risposta

12

Evan Weaver Interlock Plugin risolve questo problema.

È anche possibile implementare qualcosa di simile con facilità se si desidera un comportamento diverso, ad esempio un controllo più fine. L'idea di base è quella di avvolgere il vostro codice del controller in un blocco che è solo in realtà eseguita se la vista ha bisogno che i dati:

# in FooController#show 
@foo_finder = lambda{ Foo.find_slow_stuff } 

# in foo/show.html.erb 
cache 'foo_slow_stuff' do 
    @foo_finder.call.each do 
    ... 
    end 
end 

Se si ha familiarità con i principi fondamentali di rubino programmazione meta è abbastanza facile da avvolgere questo in su in una API più pulita dei tuoi gusti.

Questa è superiore a mettere il codice finder direttamente nella vista:

  • mantiene il codice finder in cui gli sviluppatori si aspettano che per convenzione
  • mantiene la vista ignoranti del nome del modello/metodo, consentendo una maggiore vista riuso

Penso che cache_fu potrebbe avere funzionalità simili in una delle sue versioni/fork, ma non può richiamare in modo specifico.

Il vantaggio che si ottiene da memcached è direttamente correlato al tasso di hit della cache. Fare attenzione a non sprecare la capacità della cache e causare errori non necessari memorizzando nella cache lo stesso contenuto più volte.Ad esempio, non memorizzare nella cache un set di oggetti record e il loro frammento html allo stesso tempo. Generalmente il caching dei frammenti offrirà le migliori prestazioni, ma in realtà dipende dalle specifiche dell'applicazione.

+0

Mentre penso che la risposta di Lar potrebbe essere un po 'più chiara ed evita l'uso della meta-programmazione, la tua risposta è migliore in quanto l'annuncio è più vicino a MVC. Ho svalutato entrambi come validi, ma lascerò decidere alla comunità (non ne sceglierò uno come accettato :) Grazie! –

+0

Sono d'accordo e trovo anche questa una bella soluzione. Per quanto riguarda il riutilizzo della vista, sì in generale è un buon principio. Dovresti lasciare che il controller fornisca la chiave di cache, anche se per farlo funzionare qui. A proposito, curiosamente, la documentazione API sulla cache dei frammenti viola il principio MVC nel primo esempio: http://api.rubyonrails.org/classes/ActionController/Caching/Fragments.html –

3

Cosa succede se la cache scade tra il momento in cui lo si controlla nel controller e il momento in cui viene selezionato nel rendering della vista?

mi piacerebbe fare un nuovo metodo nel modello:

class Post 
    def self.recent(count) 
     find(:all, :limit=> count, :order=>"updated_at DESC") 
    end 
    end 

quindi utilizzare che nella vista:

<% cache("recent_posts", :expires_in => 30.minutes) do %> 
    <% Post.recent(20).each do |post| %> 
    ... 
    <% end %> 
<% end %> 

Per chiarezza, si potrebbe anche prendere in considerazione di spostare il rendering di un recente post nel proprio parziale: di

<% cache("recent_posts", :expires_in => 30.minutes) do %> 
    <%= render :partial => "recent_post", :collection => Post.recent(20) %> 
<% end %> 
+0

Ottimo punto di Lars! Roba molto buona –

+0

Penso che la tua sia la soluzione migliore finora, anche se tecnicamente questo potrebbe violare MVC è abbastanza chiaro. La risposta di Jason Watkin è una valida alternativa. –

1

Si può anche voler guardare in

Fragment Cache Docs

che ti permettono di fare questo:

<% cache("recent_posts", :expires_in => 30.minutes) do %> 
    ... 
<% end %> 

controller

unless fragment_exist?("recent_posts") 
    @posts = Post.find(:all, :limit=>20, :order=>"updated_at DESC") 
end 

Anche se ammetto la questione della Secco fermo alza la testa e ha bisogno del nome della chiave in due punti. Di solito lo faccio in modo simile a quanto suggerito da Lars, ma in realtà dipende dal gusto. Gli altri sviluppatori che conosco restano con il controllo del frammento.

Aggiornamento:

Se si guarda la documentazione frammento, si può vedere come si libera di avere bisogno del prefisso vista:

# File vendor/rails/actionpack/lib/action_controller/caching/fragments.rb, line 33 
def fragment_cache_key(key) 
    ActiveSupport::Cache.expand_cache_key(key.is_a?(Hash) ? url_for(key).split("://").last : key, :views) 
end 
+0

ahh, ha senso. Il framment_exist? il metodo probabilmente elimina il bisogno di quel prefisso "view /" sul cache_key? –

1

Lars rende davvero un buon punto su che vi sia una piccola possibilità di errore utilizzando:

unless fragment_exist?("recent_posts") 

perché c'è uno spazio tra quando si controlla la cache e quando si utilizza la cache.

Il plug-in che jason menziona (Interlock) lo gestisce con molta grazia assumendo che se si sta verificando l'esistenza del frammento, probabilmente si utilizzerà anche il frammento e quindi si memorizzerà il contenuto localmente. Io uso Interlock proprio per questi motivi.

1

solo come un pezzo di pensiero:

controller dell'applicazione definiscono

def when_fragment_expired(name, time_options = nil) 
     # idea of avoiding race conditions 
     # downside: needs 2 cache lookups 
     # in view we actually cache indefinetely 
     # but we expire with a 2nd fragment in the controller which is expired time based 
     return if ActionController::Base.cache_store.exist?('fragments/' + name) && ActionController::Base.cache_store.exist?(fragment_cache_key(name)) 

     # the time_fraqgment_cache uses different time options 
     time_options = time_options - Time.now if time_options.is_a?(Time) 

     # set an artificial fragment which expires after given time 
     ActionController::Base.cache_store.write("fragments/" + name, 1, :expires_in => time_options) 

     ActionController::Base.cache_store.delete("views/"+name)   
     yield  
    end 

poi in qualsiasi uso azione

def index 
when_fragment_expired "cache_key", 5.minutes 
@object = YourObject.expensive_operations 
end 
end 

in vista

cache "cache_key" do 
view_code 
end