Блог Ruby-разработчика

Кэширование коллекций в Rails 5

| Comments

Кэширование в Rails позволяет повысить производительность вашего приложения. В Rails 5 теперь можно кешировать коллекции записей, благодаря методу: ActiveRecord::Relation#cache_key

Что из себя представляет кэширование коллекций

Рассмотрим пример, в котором нам нужно получить коллекцию пользователей с определенным именем.

1
@users = User.where(name: 'Михаил')

В переменной @users мы получим некую коллекцию записей, которая будет являться объектом класса ActiveRecord::Relation Результат нашего запроса не изменится если будут выполнены следующие условия:

  • Мы будем искать также Михаила
  • За время нашего запроса записи не были удалены
  • За время нашего запроса не было добавлено новых записей

Rails комьюнити предложило реализовать кэширование для коллекций. Метод cache_key был добавлен в ActiveRecord::Relation, и он учитывает множество факторов, включая изменение запроса, updated_at для запроса, и значение счетчика записей в коллекции.

ActiveRecord::Relation#cache_key

Итак у нас есть коллекция пользователей, давайте вызовем у этой коллекции метод cache_key

1
2
@users.cache_key
 => "users/query-67sa32b36805c4b1ec1948b4eef8d58f-3-20160116111659084027"

Мы получим вот такую запись. Сейчас подробнее рассмотрим что она означает.

  • users представляет название коллекции, основанной на модели User
  • query- не меняющаяся часть, всегда будет присутствовать
  • 67sa32b36805c4b1ec1948b4eef8d58f md5 сумма, котрая образуется из SQL. В нашем примере это MD5("SELECT "users".* FROM "users" WHERE "users"."name" = 'Михаил'")
  • 3 размер коллекции
  • 20160116111659084027 временная метка(timestamp). Это значение соответствует самому свежему updated_at в коллекции.

Как использовать?

Один из примеров, как можно применить cache_key

1
2
3
4
5
6
7
users = User.where(name: 'Михаил')

if users.cache_key == current_cache_key
 # не используем запрос, и возвращаем кешированную страницу
else
 # получаем данные из базы данных
end

В модели нет колонки :updated_at. Что делать?

По умолчанию, для создания timestamp, cache_key использует колонку :updated_at Это дефолтная реализация. Если у вас этой колонки нет, но есть другая, с названием отличным от стандартного, вы можете использовать ee, указав как аргумент в вызове метода cache_key

1
2
3
products = Product.where(category: 'cars')
 products.cache_key(:last_bought_at)
 => "products/query-211ae6b96ec456b8d7a24ad5fa2f8ad4-4-20160118080134697603"

Особенности использования

Использование limit в запросах

Рассмотрим 2 варианта. Если мы хотим получить 3 пользователя с именем ‘Михаил’, то cache_key корректно работает.

1
2
3
users = User.where(name: 'Михаил').limit(3)
users.cache_key
 => "users/query-67ed32b36805c4b1ec1948b4eef8d58f-3-20160116144936949365"

Но если мы вызовем cache_key не извлекая записи в @users, то получим следующую картину.

1
2
User.where(name: 'Алексей').limit(3).cache_key
 => "users/query-8dc512b1408302d7a51cf1177e478463-5-20160116144936949365"

В итоге мы получаем всех пользователей, и наш limit не учитывается. Это особенность реализации метода. ActiveRecord::Base#collection_cache_key.

Cache_key не меняется, при изменении порядка записей в коллекции

Например:

1
2
3
users1 = User.where(name: 'Михаил').order('id desc').limit(3)
users1.cache_key
 => "users/query-648522f6b3a53e0a7f7b63d5967afaf7-3-20160207084207845084"

В итоге мы получаем пользователей с ids - [5, 4, 3] Теперь попробуем удалить пользователя с id = 3

1
2
3
4
5
User.find(3).destroy

 users2 = User.where(name: 'Михаил').order('id desc').limit(3)
 users2.cache_key
 => "users/query-648522f6b3a53e0a7f7b63d5967afaf7-3-20160207084207845084"

Обратите внимание, что cache_key совпадают в обоих примерах. Это происходит потому, что ни один из параметров, влияющих на ключ кэша не изменяется. Т.е. ни количество записей, ни запрос, ни метка времени последней записи.

Использование group в запросах

Также как и в случае с limit, cache_key ведет себя по-разному, в случаях когда записи уже есть в памяти и когда их еще нет. Допустим у нас есть несколько пользователей с одинаковыми именами:

1
2
User.select(:name, :updated_at).group(:name).cache_key
 => "users/query-1eabedee52c695b4c97f6d560211e970-3-20160207084207845084"

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

1
2
3
@users = User.select(:name, :updated_at).group(:name)
@users.cache_key
 => "users/query-1eabedee52c695b4c97f6d560211e970-1-20160207084207845084"

Теперь размер коллекции составляет 1. Т.е кол-во групп. Эти особенности нужно учитывать, иначе они могут сыграть злую шутку.

Comments