Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Rails 7: ActiveRecord::Coreのfindがfind_byキャッシュキーを再利用するようになった(翻訳)

概要

元サイトの許諾を得て翻訳・公開いたします。

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をサポート(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。