#23 Counter Cache Column
Come nel’episodio precedente, ci concentreremo sulle performance. Sotto abbiamo un’applicazione che mostra una lista di progetti assieme al numero di task ad essi associati.
Il ProjectsController e la vista index sono qui di seguito mostrati:
class ProjectsController < ApplicationController def index @projects = Project.find(:all) end end
Il ProjectsController
.
<h1>Projects</h1> <ol> <% @projects.each do |project| %> <li><%= link_to project.name, project_path(project) %> (<%= pluralize project.tasks.size, ’task’ %>)</li> <% end %> </ol>
La vista index
.
Nella vista c’è un ciclo per ogni Project
, che serve a mostrare il nome del progetto e il numero di task (ottenuti con projects.tasks.size
) che ha associati. Si usa anche il pluralize
in modo tale da mostrare "task" o "tasks" a seconda del fatto che i task associati siano uno o più.
Migliorare gli accessi al database
Diamo un’occhiata ai log di sviluppo per vedere in che modo stiamo accedendo al databasequando carichiamo la pagina index
:
Rendering projects/index SQL (0.3ms) SELECT count(*) AS count_all FROM "tasks" WHERE ("tasks".project_id = 61) SQL (0.2ms) SELECT count(*) AS count_all FROM "tasks" WHERE ("tasks".project_id = 62) SQL (0.3ms) SELECT count(*) AS count_all FROM "tasks" WHERE ("tasks".project_id = 63) SQL (0.2ms) SELECT count(*) AS count_all FROM "tasks" WHERE ("tasks".project_id = 64) SQL (0.2ms) SELECT count(*) AS count_all FROM "tasks" WHERE ("tasks".project_id = 65)
Viene fatta una chiamata al database per ogni progetto presente nella lista, per ottenere un conteggio dei task che ogni progetto ha. Come possiamo ridurre il numero di query fatte? Uno dei modi potrebbe essere quello di utilizzare l’eager loading, come abbiamo visto nell’episodio precedente. Lo faremmo, cambiando il ProjectsController
in modo tale da fargli prendere i task insieme ai modelli di progetto:
@projects = Project.find(:all, :include => :tasks)
Ora, ricaricando la pagina, vedremmo che il numero di richieste al database si è ridotto a due:
Processing ProjectsController#index (for 127.0.0.1 at 2009-01-26 21:24:28) [GET] Project Load (1.1ms) SELECT * FROM "projects" Task Load (7.1ms) SELECT "tasks".* FROM "tasks" WHERE ("tasks".project_id IN (61,62,63,64,65))
Questa è sicuramente una miglioria, ma comporta un traffico di informazione inutile, in quanto ci stiamo caricando tutte le informazioni sui task, pur avendo bisogno di sapere solo il loro numero per progetto. Invece di usare l’eager loading, dunque, useremo la colonna di conteggio di cache:
Implementare una colonna di conteggio cache
La prima cosa da fare è di aggiungere una colonna alla tabella Projects
che contenga il numero di Task
associati al progetto. Generiamo la nuova colonna con una migration:
script/generate migration add_tasks_count
La nostra migration sarà così fatta. La spiegheremo sotto:
class AddTasksCount < ActiveRecord::Migration def self.up add_column :projects, :tasks_count, :integer, :default => 0 Project.reset_column_information Project.all.each do |p| p.update_attribute :tasks_count, p.tasks.length end end def self.down remove_column :projects, :tasks_count end end
Il nome dato alla colonna è fondamentale. Deve essere esattamente lo stesso del nome del modello che vogliamo contare, seguito da _count
. Anche il valore di default è importante. Se non fosse 0, il conteggio non funzionerebbe correttamente. Una volta creata la nuova colonna, c’è bisogno di impostare il valore della colonna count per ogni record di progetto. Per farlo, iteriamo su ogni progetto e impostiamo il suo attributo tasks_count
al numero di task che il progetto ha. Usiamo length
anzichè size
per avere il numero di task, poichè size
userebbe la colonna appena creata per risponderci, che conterrebbe il valore di default 0.
Dal momento che stiamo un Project
nella stessa migration in cui aggiungiamo una colonna alla sua tabella, cè la possibilità che l’informazione della nuova colonna possa essere cached. E’ buona norma sincerarsi che sia resettata e lo facciamo con Project.reset_column_information
.
Ha funzionato?
Ora che abbiamo aggiunto la colonna, rimuoviamo l’eager loading dal ProjectsController
e ricarichiamo la pagina.
Processing ProjectsController#index (for 127.0.0.1 at 2009-01-26 22:07:13) [GET] Project Load (0.7ms) SELECT * FROM "projects"
Ora c’è una sola chiamata al database, in quanto tutte le informazioni di cui abbiamo bisogno sono presenti nella tabella dei Projects
. Il numero di task per Project
ora viene fornito dalla colonna tasks_count
.
Ultimo passo
C’è un ultimo passo da compiere. Se aggiungiamo un nuovo task ad un progetto, la colonna di conteggio non sarebbe aggiornata per com’è il codice ora, perchè non abbiamo detto a Rails di usare la colonna tasks_count
come colonna di cache di conteggio. Lo facciamo ora, aggiornando il modello Task
:
class Task < ActiveRecord::Base belongs_to :project, :counter_cache => true has_many :comments end
Stiamo dicendo a Rails di usare per l’associazione verso i progetti una colonna di cache di conteggio, mediante l’aggiunta dell’opzione :counter_cache => true
all’associazione stessa. Ora usiamo la console per aggiungere un nuovo task ad un progetto:
>> p = Project.first => #<Project id: 61, name: "Project 1", created_at: "2009-01-26 20:34:36", updated_at: "2009-01-26 22:05:22", tasks_count: 20> >> p.tasks.create(:name => "New task") => #<Task id: 1201, name: "New task", project_id: 61, created_at: "2009-01-26 22:24:13", updated_at: "2009-01-26 22:24:13">
Aggiunta di un nuovo task mediante console.
Ora ricarichiamo la pagina e vedremo come la colonna di conteggio si sia aggiornata.
La colonna di cache di conteggio del progetto che è stata aggiornata.
Il nostro primo progetto ha ora 21 task e siamo riusciti ad accedere esclusivamente alla tabella inerente ai modelli master (progetti) da mostrare.