Fragment
Caching im Development-Modus aktivieren
Als Erstes müssen wir in der Datei
config/environments/development.rb
den Eintrag
config.action_controller.perform_caching
auf
true
setzen:
config.action_controller.perform_caching = true
Sonst können wir das Fragement Caching im Development-Modus nicht
ausprobieren. Im Production-Modus ist Fragement Caching per Default
aktiviert.
Tabelle des
Index-View cachen
Auf der Seite
http://0.0.0.0:3000/companies
wird eine sehr rechenintensive Tabelle mit allen Firmen gerendert. Diese
Tabelle können wir gesamt cachen. Dazu müssen wir die Tabelle in einen
<% cache('name_des_caches') do %> ... <% end
%>
-Block einbauen:
<% cache('name_des_caches') do %>
[...]
<% end %>
Bitte verändern Sie die Datei
app/views/companies/index.html.erb
wie
folgt:
<h1>Listing companies</h1>
<% cache('table_of_all_companies') do %>
<table>
<tr>
<th>Name</th>
<th>Number of employees</th>
<th></th>
<th></th>
<th></th>
</tr>
<% @companies.each do |company| %>
<tr>
<td><%= company.name %></td>
<td><%= company.employees.count %></td>
<td><%= link_to 'Show', company %></td>
<td><%= link_to 'Edit', edit_company_path(company) %></td>
<td><%= link_to 'Destroy', company, method: :delete, data: { confirm: 'Are you sure?' } %></td>
</tr>
<% end %>
</table>
<% end %>
<br />
<%= link_to 'New Company', new_company_path %>
Danach können Sie den Rails-Server mit
rails
server starten und die URL
http://0.0.0.0:3000/companies
aufrufen. Im Development-Log werden Sie dann den folgenden Eintrag
finden:
Write fragment views/table_of_all_companies (2.9ms)
Rendered companies/index.html.erb within layouts/application (119.8ms)
Completed 200 OK in 209ms (Views: 143.1ms | ActiveRecord: 15.0ms)
Das Schreiben des Caches hat 2.0 ms gedauert. Insgesamt hat das
Rendern der Seite 209 ms gedauert.
Bei einem wiederholten Aufruf der gleichen Seite bekommen Sie eine
andere Ausgabe im Log:
Read fragment views/table_of_all_companies (0.2ms)
Rendered companies/index.html.erb within layouts/application (0.8ms)
Completed 200 OK in 37ms (Views: 34.6ms | ActiveRecord: 0.3ms)
Das Lesen des Caches dauerte 0.2 ms und das gesamte Rendern der
Seite 37 ms. Nur ein Fünftel Rechenzeit!
Mit der Methode
expire_fragment
können
Sie gezielt Fragment Caches löschen. Von der Grundidee können wir das
genauso im Model einbauen wie in
„Page Caches
automatisch löschen“ gezeigt.
Die Model-Datei
app/models/company.rb
würde
dann so aussehen:
class Company < ActiveRecord::Base
attr_accessible :name
validates :name,
:presence => true,
:uniqueness => true
has_many :employees, :dependent => :destroy
after_create :expire_cache
after_update :expire_cache
before_destroy :expire_cache
def to_s
name
end
def expire_cache
ActionController::Base.new.expire_fragment('table_of_all_companies')
end
end
Da sich die Anzahl der Employees auch auf diese Tabelle auswirkt,
müssten wir auch die Datei
app/models/employees.rb
entsprechend erweitern:
class Employee < ActiveRecord::Base
attr_accessible :company_id, :first_name, :last_name, :phone_number
belongs_to :company, :touch => true
validates :first_name,
:presence => true
validates :last_name,
:presence => true
validates :company,
:presence => true
after_create :expire_cache
after_update :expire_cache
before_destroy :expire_cache
def to_s
"#{first_name} #{last_name}"
end
def expire_cache
ActionController::Base.new.expire_fragment('table_of_all_companies')
end
end
Das gezielte Löschen von Fragment Caches ist programmiertechnisch
oft aufwendig. Erstens übersieht man häufig etwas und zweitens ist es
auch nicht einfach, in großen Projekten über die verschiedenen Namen der
Caches den Überblick zu bewahren. Oft ist es einfacher, mit der
cache_key
-Methode automatische Namen zu
erstellen, die im Cache automatisch altern (siehe
„Auto-Expiring
Caches“).
Die Verwaltung von Fragment Caches ist bei der in
„Tabelle des
Index-View cachen“ verwendeten
Namenskonvention nicht trivial. Man kann zwar bei sauberer
Programmierung sicher sein, dass der Cache keinen überflüssigen Ballast
mitschleppt, aber auf der anderen Seite kann einem das auch eigentlich
egal sein. Ein Cache ist immer so aufgebaut, dass er selbstständig alte
und nicht benötigte Elemente löscht. Wenn wir ähnlich wie in der Asset
Pipeline (siehe
Kapitel 12, Asset Pipeline) einen Mechanismus
einsetzen, der einen Fragment Cache unique benennt, dann müssten wir
nicht aufwendig Fragment Caches löschen.
Genau dafür gibt es die Methode
cache_key
.
cache_key
gibt Ihnen einen unique Namen für ein Element. Probieren wir es einmal
in der Console aus. Als Erstes lassen wir uns dreimal den immer
identischen
cache_key
des ersten Company
Eintrages geben ("companies/2-20120716190032"), dann touchen wir den
Eintrag (mit einem
touch
wird das Attribute
updated_at
auf die aktuelle Uhrzeit gesetzt) und zum
Schluss geben wir noch mal dreimal den neuen
cache_key
aus
("companies/2-20120716192035"):
MacBook:phone_book xyz$ rails console
Loading development environment (Rails 3.2.6)
1.9.3p194 :001 > Company.first.cache_key
Company Load (0.1ms) SELECT "companies".* FROM "companies" LIMIT 1
=> "companies/2-20120716190032"
1.9.3p194 :002 > Company.first.cache_key
Company Load (0.3ms) SELECT "companies".* FROM "companies" LIMIT 1
=> "companies/2-20120716190032"
1.9.3p194 :003 > Company.first.cache_key
Company Load (0.3ms) SELECT "companies".* FROM "companies" LIMIT 1
=> "companies/2-20120716190032"
1.9.3p194 :004 > Company.first.touch
Company Load (0.3ms) SELECT "companies".* FROM "companies" LIMIT 1
SQL (2.0ms) UPDATE "companies" SET "updated_at" = '2012-07-16 19:20:35.223146' WHERE "companies"."id" = 2
=> true
1.9.3p194 :005 > Company.first.cache_key
Company Load (0.3ms) SELECT "companies".* FROM "companies" LIMIT 1
=> "companies/2-20120716192035"
1.9.3p194 :006 > Company.first.cache_key
Company Load (0.3ms) SELECT "companies".* FROM "companies" LIMIT 1
=> "companies/2-20120716192035"
1.9.3p194 :007 > Company.first.cache_key
Company Load (0.3ms) SELECT "companies".* FROM "companies" LIMIT 1
=> "companies/2-20120716192035"
1.9.3p194 :008 > exit
MacBook:phone_book xyz$
Verändern wir einmal mit diesem Wissen den Index-View in der Datei
app/views/companies/index.html.erb
:
<h1>Listing companies</h1>
<% cache(@companies) do %>
<table>
<tr>
<th>Name</th>
<th>Number of employees</th>
<th></th>
<th></th>
<th></th>
</tr>
<% @companies.each do |company| %>
<% cache(company) do %>
<tr>
<td><%= company.name %></td>
<td><%= company.employees.count %></td>
<td><%= link_to 'Show', company %></td>
<td><%= link_to 'Edit', edit_company_path(company) %></td>
<td><%= link_to 'Destroy', company, method: :delete, data: { confirm: 'Are you sure?' } %></td>
</tr>
<% end %>
<% end %>
</table>
<% end %>
<br />
<%= link_to 'New Company', new_company_path %>
Wir benutzen hier nicht nur einen Fragment Cache für die ganze
Tabelle, sondern auch jeweils einen für jede Zeile. Der erste Aufruf
dauert damit auch länger als vorher. Aber wenn sich einzelne Firmen
ändern, muss immer nur eine Zeile neu gerendet werden.
Anmerkung
In welcher Detailtiefe man Fragment Cacheing einsetzen sollte,
kann nicht allgemein beantwortet werden. Experimentieren Sie damit und
schauen Sie sich im Log an, was wie lange dauert.
Software-Versionierung
und current_user
Als Entwickler sollten Sie beim Einsatz von Fragment Caches immer
einen Versionsstand mit eincodieren. Nur so können Sie sicher sein, dass
bei einer neuen Software-Version nicht ein alter Cache mit einem alten
Inhalt ausgeliefert wird. Sie können dazu den Namen des Fragment Caches
nicht als String, sondern als Array angeben. Beispiel:
<% cache(['V3.23', @companies]) do %>
[...]
<% end %>
Über diesen Mechanismus können Sie auch Caches für einzelne User
realisieren. Wenn Sie ein User-Objekt mit dem Namen
current_user
haben, können Sie folgenden Code zum Fragment
Cachen benutzen:
<% cache(['V3.23', current_user, @companies]) do %>
[...]
<% end %>
Wenn kein User eingeloggt ist, dann ist current_user
nil und funktioniert dann für alle nicht angemeldeten User.
Tipp
Es sprengt den Rahmen eines Anfänger-Buches, aber bei der
Benutzung einer Versionsverwaltung von Dateien (z. B. git) ist es
praktisch, anstatt des 'V3.23' eine Konstante einzubauen und diese in
einem Initializer mit dem aktuellen Commit SHA aus dem benutzten
Repository zu setzen.
Der Cache Store verwaltet die gespeicherten Fragment Caches. Wenn
nicht anders konfiguriert, ist dies der MemoryStore von Rails. Dieser
Cache Store ist gut zum Entwickeln geeignet, aber für ein
Produktivsystem weniger, weil er pro Ruby on Rails Prozess eigenständig
agiert. Wenn Sie also im Produktivsystem mehrere Ruby on Rails Prozesse
parallel laufen lassen, dann hält jeder Prozess einen eigenen
MemoryStore vor.
Die meistens Produktivsysteme benutzen memcached (
http://memcached.org/
) als
Cache Store. Um memcached als Cache Store im Produktivsystem zu
aktivieren, müssen Sie in der Datei
config/environments/production.rb
die folgende
Zeile hinzufügen:
config.cache_store = :mem_cache_store
Zusätzlich müssen Sie in der Datei Gemfile noch einen Eintrag
für das Laden des memcache-client Gems eintragen:
group :production do
gem 'memcache-client'
end
Danach müssen Sie ein
bundle install
ausführen:
Stefan-Wintermeyers-MacBook-Air:phone_book xyz$ bundle install
[...]
Stefan-Wintermeyers-MacBook-Air:phone_book xyz$
Die Kombination von sinnvoll eingesetzten Auto-Expiring Caches
und memcached ist ein sehr gutes Rezept für eine performante
Webseite.