Neu: Rails Schulungen in Deutschland vom Autor dieses Buches.div class="para">Mit Fragment Caching können Sie einzelne Teile eines Views cachen. Es kann problemlos zusammen mit Abschnitt 14.2, „HTTP-Caching“ und Abschnitt 14.3, „Page Caching“ eingesetzt werden. Die Vorteile liegen auch hier in der Reduzierung der Server-Load und der schnelleren Webseitengenerierung und damit einer besseren Usability.

Bitte installieren Sie eine frische Beispielapplikation (siehe „Eine einfache Beispielapplikation“).

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!

Fragment Cache löschen

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“).

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.

Cache Store

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.

MemCacheStore

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.
Eine Installationsbeschreibung für ein Rails-Produktivsystem mit memcached finden Sie in Kapitel 15, Webserver im Produktionsbetrieb.

Andere Cache Stores

In der offiziellen Rails Doku finden Sie unter http://guides.rubyonrails.org/caching_with_rails.html#cache-stores eine Liste mit anderen Cache Stores.

Autor

Stefan Wintermeyer