Hashを使って大量のActiveRecordをキャッシュに載せる方法
ループ処理の中で、あるテーブルに対してアクセスしなければならない場合、ループ毎にテーブルをfindするのは非効率かもしれません。事前に必要なテーブルデータを抽出しておき、そのキャッシュを使って処理するほうが効率的な場合があります。
しかし、このような実装方法をとると、取り扱うデータ量によってはメモリを逼迫する要因になり兼ねません。
そこで、なるべくメモリを消費せずに大量データをキャッシュさせる方法を考えてみようと思います。
ActiveRecordオブジェクトをHashにする
一般的な方法は対象オブジェクトをHashにする方法です。
keyを対象オブジェクトのid、valueを対象オブジェクトとしたHashにします。
この方法は実装がシンプルなので使われるケースが多いかと思います。
ただし、ActiveRecordオブジェクト自体がキャッシュに乗ることになるので、大量データを扱う場合は多くのメモリを消費してしまいます。
# 対象オブジェクトをHashにする
users = User.all.map {|d| [d.id, d] }.to_h
# または以下でも同様の結果が得られます。
users = User.all.index_by(&:id)
# 取得する場合
user = users[user_id]
ActiveRecordオブジェクトから特定の項目のみHashにする
データベースから取得した結果をmapで必要な項目のみ抽出して、Hashにする方法です。
サンプルコードのようにキャッシュに乗るのはid、 first_name、last_name、ageのみに限定されるように見えます。
項目を絞っているので、オブジェクト自体を取得するよりもメモリの消費を抑えることができます。
ただし、この方法だとallで取得したデータを一度キャッシュに乗せる必要があるので、一時的にメモリ消費量が大きくなります。
メモリ使用量は時間とともに徐々に落ち着いてきますが、メモリリークの引き金になるかもしれません。
users = User.all.map do |user
[user.id,
{
first_name: user.first_name,
last_name: user.last_name,
age: user.age
}]
end.to_h
ActiveRecordオブジェクトをpluckしてからHashにする
pluckを使用することでSELECT句レベルで項目が絞られた状態からHashにすることができます。
直接all.mapするよりも取得するデータ量が減るのでメモリ消費が抑えられます。
pluckすることにより可読性は低下しますが、パフォーマンスを重視したい場合はこの方法を取ると良いでしょう。
users = User.all.pluck(:id, :first_name, :last_name, :age).map do |user
[user[0],
{
first_name: user[1],
last_name: user[2],
age: user[3]
}]
end.to_h
Discussion