Pull to refresh

Тонкости Rails 4 — Cache Digests

Reading time 5 min
Views 13K
Original author: Ryan Bates


Гем под названием "cache_digests" (включен по умолчанию в Rails 4) автоматически добавляет цифровую подпись к каждому фрагментному кэшу, основываюсь на представлении (вьюхе). При этом, если страница изменяется, то старый кэш автоматически удаляется. Но остерегайтесь подводных камней!



Cодержание цикла «Тонкости Rails 4»



Я написал небольшое приложение, в котором имеется список с проектами, у каждого из которых есть определенный список задач. Предположим, что в данном приложении возникли проблемы с производительностью и для их исправления было принято решение воспользоваться фрагментным кэшированием.



Следующий код отображает список проектов:

/app/views/projects/index.html.erb
<h1>Projects</h1>
<%= render @projects %>

Для каждого проекта генерируется партиал _project. Он тоже весьма прост и занимается отображением списка задач:

/app/views/projects/_project.html.erb
<h2><%= link_to project.name, edit_project_path(project) %></h2>
<ul><%= render project.tasks %></ul>

В свою очередь, _project рендерит еще один партиал: _task. Итак, добавим фрагментное кэширование для _project.

/app/views/projects/_project.html.erb
<% cache project do %>
  <h2><%= link_to project.name, edit_project_path(project) %></h2>
  <ul><%= render project.tasks %></ul>
<% end %>

Так как вышеприведенный код отображает список задач, то было бы разумно переставать кэшировать старые данные при появлении новой задачи. Эту цель можно достичь добавив в связь с проектом touch: true в модель Task:

/app/models/task.rb
class Task < ActiveRecord::Base
  attr_accessible :name, :completed_at
  belongs_to :project, touch: true
end

Теперь при изменении задачи проекта она будет помечена как обновленная. Проверим работу кэширования в режиме development:

/config/development.rb
config.action_controller.perform_caching = true

После рестарта сервера и обновления страницы каждый из проектов будет кэшироваться с помощью фрагментного кэширования. В то же время если одна из задач будет отредактирована, то срок действия кэша истечет и таким образом будут загружены новые данные.

Все это замечательно, но что произойдет если изменения будут внесены в код самой страницы? К примеру, я обновил код для отображения задач в виде нумерованного списка:

/app/views/projects/_project.html.erb
<% cache project do %>
  <h2><%= link_to project.name, edit_project_path(project) %></h2>
  <ol><%= render project.tasks %></ol>
<% end %>

Теперь обновим страницу в браузере. Никаких видимых изменений не произошло! Это случилось из-за того что страница со старым кодом уже сохранилась в кэше, и срок его действия еще не истек. Поэтому старый контент по-прежнему виден. Эту проблему обычно обходят путем обновления версии ключа кэша:

/app/views/projects/_project.html.erb
<% cache ['v1', project] do %>
  <h2><%= link_to project.name, edit_project_path(project) %></h2>
  <ol><%= render project.tasks %></ol>
<% end %>

Так как значение ключа было изменено, то старый кэш стал невалиден и на странице отображаются задачи с нумерованным списком. Ура!



Но есть некоторая проблема. Нужно постоянно помнить, что при каждом изменении кода страницы необходимо также поменять номер версии кэша для вступления новых изменений в силу. В принципе, это нетрудно, но все мгновенно усложняется если используется вложенное фрагментное кэширование. Предположим, что я также хочу закэшировать партиал с задачами с целью еще немного увеличить производительность:

/app/views/tasks/_task.html.erb
<% cache ['v1', task] do %>
  <li>
    <%= task.name %>
    <%= link_to "edit", edit_task_path(task) %>
  </li>
<% end %>

Теперь нужно также обновить ключ кэша в партиале с проектом для того, чтобы весь старый кэш исчез.

То есть, к примеру, если мы обновим партиал с задачей, изменив имя ссылки с «edit» на «rename» то очевидно, что необходимо поменять его ключ кэша. Но никаких видимых изменений на странице не произойдет до тех пор пока также не изменится значение ключа в партиале с проектами. И только после этого мы увидим наши долгожданные нововведения:



Сache digests


Да, такое кэширование работает, но, согласитесь, оно ужасно. И тут к нам на помощь приходит гем под названием «cache_digests»! Его функционал включен в Rails 4, но также он был выделен с отдельный гем для того, чтобы разработчики могли использовать его уже сегодня в проектах с Rails 3.

Этот гем включает цифровую подпись в фрагментный кэш, базируясь на представлениях. Это значит, что изменения в коде страницы будут также менять ключ кэша, очищая таким образом старый.

Давайте опробуем его работу. Для этого нужно включить в gemfile следующую строку:

/Gemfile
gem 'cache_digests'

И затем:
$ bundle install

Теперь нет более необходимости в указании версии ключа и поэтому можно со спокойной совестью удалить лишний код из партиалов _project и _task. После этого необходимо перезапустить сервер и обновить страницу в браузере для вступления в силу нового кэширования.

Если этого не сделать, и при этом попробовать немного поменять код вьюхи проектов и обновить страничку, то изменения не произойдут. Причина кроется в том, что гем «cache digest» не анализирует изменения в представлениях при каждом изменении кода, ведь это крайне неразумно. Вместо этого он хранит свой локальный кэш для каждого представления, и каждому ставит в соответствие уникальную цифровую подпись.

Чтобы увидеть изменения в режиме development необходимо рестартануть сервер нашего приложения. Такие проблемы не должны появляться в продакшене, так как при каждом новом деплое все равно перезапускается сервер.

Теперь, обновив страницу, будет видно, что обновления в коде не остались незамеченными гемом и перед нами наконец-то предстала обновленная страница. Кстати говоря, гем достаточно умен и умеет определять зависимости. Ну вот, к примеру, мы еще помним, что представление с проектами вызывает метод render для отображения списка задач. Поэтому, очевидно, что если партиал с задачами вдруг изменился, то возникает необходимость в удалении старого кэша в вьюхе с проектами.

Подводные камни


Но все же не стоит сильно расслабляться, так как возможны случаи, в которых зависимости не будут верным образом определены. Рассмотрим небольшой пример.

Допустим, в модели Project существует метод incomplete_tasks. И я решил воспользоваться этим методом для отображения неоконченных задач в партиале (отвечающим за отображение списка проектов). Если это сделать, то станет видно, что изменения в вьюхе не были отображены, поскольку зависимости не были определены верно. Пожалуй, неплохой идеей в данном случае будет запуск rake задачи cache_digests:nested_dependencies, столь любезно предоставленный гемом.

$ rake cache_digests:nested_dependencies TEMPLATE=projects/index
[
  {
    "projects/project": [
      "incomplete_tasks/incomplete_task"
    ]
  }
]

Как видно, из вышеприведенного кода передается путь к необходимой вьюхе для анализа возникшей проблемы.

Вывод rake задачи показывает, что была найдена зависимость в партиале с проектами (что хорошо), но при этом определена она неверно: на месте incomplete_task должен находиться task. Для того, чтобы исправить сей неприятный казус рекомендую воспользоваться следующим кодом (обратите внимание, что я указываю partial и использую collection):

/app/views/projects/_project.html.erb
<% cache project do %>
  <h2><%= link_to project.name, edit_project_path(project) %></h2>
  <ul><%= render partial: 'tasks/task', collection: project.incomplete_tasks %></ul>
<% end %>

Снова запустив тот же rake task станет видно, что зависимости определены теперь должным образом и кэш был успешно обновлен!

$ rake cache_digests:nested_dependencies TEMPLATE=projects/index
[
  {
    "projects/project": [
      "tasks/task"
    ]
  }
]

Более подробно о работе гема можно почитать в его README, что я и рекомендую сделать всем заинтересовавшимся. Спасибо за внимание!

Обо всех найденных ошибках, неточностях перевода и прочих подобных вещах просьба сообщать в личку.



Приложение
Исходный код приложения из урока


Подписывайтесь на мой блог!
Tags:
Hubs:
+22
Comments 38
Comments Comments 38

Articles