Rails 7: ActiveRecord::Coreのfindがfind_byキャッシュキーを再利用するようになった(翻訳)
#find
や#find_by
でクエリを実行すると、結果はキャッシュに保存され、データベースサーバーに負荷をかけずにキャッシュからクエリ結果を読み込めるようになります。キャッシュキーの生成と結果のキャッシュへの保存は各クエリの責務になるため、このとおりにならないケースがいくつか現れます。
改修前
#find
と#find_by(id: ...)
は使うキャッシュキーが異なるという小さな見落としがありました。どちらのクエリも同じ結果を返しますが、キャッシュの保存場所は別々になってしまいます。
ActiveRecord::Core
の動作を見てみましょう。
def find(*ids) # :nodoc:
# これらにはまだキャッシュキーがない
return super unless ids.length == 1
return super if block_given? ||
primary_key.nil? ||
scope_attributes? ||
columns_hash.key?(inheritance_column) && !base_class?
id = ids.first
return super if StatementCache.unsupported_value?(id)
key = primary_key
statement = cached_find_by_statement(key) { |params|
where(key => params.bind).limit(1)
}
record = statement.execute([id], connection)&.first
unless record
raise RecordNotFound.new("Couldn't find #{name} with '#{key}'=#{id}", name, key, id)
end
record
end
このキャッシュキーはprimary_key
(ほとんどのシナリオでは"id"
)だけに効くことがわかります。
今度は#find_by
を見てみましょう。これは属性のハッシュを受け取ります。
def find_by(*args) # :nodoc:
return super if scope_attributes? || reflect_on_all_aggregations.any? ||
columns_hash.key?(inheritance_column) && !base_class?
hash = args.first
return super if !(Hash === hash) || hash.values.any? { |v|
StatementCache.unsupported_value?(v)
}
return super unless hash.keys.all? { |k| columns_hash.has_key?(k.to_s) }
keys = hash.keys
statement = cached_find_by_statement(keys) { |params|
wheres = keys.each_with_object({}) { |param, o|
o[param] = params.bind
}
where(wheres).limit(1)
}
begin
statement.execute(hash.values, connection)&.first
rescue TypeError
raise ActiveRecord::StatementInvalid
end
end
このキャッシュキーはhash.keys
に設定されます。これはfind_by
で検索するカラムの配列を返します。
ここで曖昧さが生じます。#find
のキャッシュキーは"id"
を返しますが、find_by
のキャッシュキーは["id"]
を返すからです。
改修後
ActiveRecord::Core
の#find
が#find_by
のキャッシュキーを再利用するようになりました(#43960)。以下の2つのクエリは、どちらも同じ場所にあるキャッシュを利用します。
クエリ | キャッシュキー |
---|---|
find(123) |
["id"] |
find_by(id: 123) |
["id"] |
find_by(id: 123, foo: true) |
["id", "foo"] |
改修内容は、#find
メソッドがprimary_key
を配列に入れるというシンプルなものです。
def find(*ids) # :nodoc:
# これらにはまだキャッシュキーがない
return super unless ids.length == 1
return super if block_given? || primary_key.nil? || scope_attributes?
id = ids.first
return super if StatementCache.unsupported_value?(id)
cached_find_by([primary_key], [id]) ||
raise(RecordNotFound.new("Couldn't find #{name} with '#{primary_key}'=#{id}", name, primary_key, id))
end
コアライブラリがほんのわずか改修されただけで、このようにアプリケーション全体で大きなメリットが生まれることもあります。
関連記事
Rails 7: delegated_typeでaccepts_nested_attributes_forをサポート(翻訳)
概要
元サイトの許諾を得て翻訳・公開いたします。