コード日進月歩

しんくうの技術的な小話、メモ、つれづれ、など

Railsのfind***by系のメソッドを軽くまとめる

以下のツイートが気になったの調べた。

環境

$ bin/rails -v
Rails 6.0.3.1

一覧

メソッド名 説明
find_or_create_by 対象を検索して、ない場合はCREATE(INSERT文の発行)をする
find_or_initialize_by 対象を検索して、ない場合はオブジェクトを作成(.new相当)をする
create_or_find_by 新規作成を試みて、 ActiveRecord::RecordNotUnique の場合は既にあるのでfindをする

find_orcreateinitialize の違い

実装を見てもらってもわかりやすい話だが、実際にcreateまで実行してしまうか、オブジェクト生成のみ留めるかの違い。

def find_or_create_by(attributes, &block)
  find_by(attributes) || create(attributes, &block)
end

def find_or_create_by!(attributes, &block)
  find_by(attributes) || create!(attributes, &block)
end

create_or_find_by について

このメソッドに関しては実態のソースに以下のようなコメントがある

1つまたは複数の列に一意の制約を持つテーブル内に、指定された属性を持つレコードを作成しようとします。これらの属性を持つ行が既に存在する場合は のような一意の制約がある場合、そのような挿入が通常発生するであろう例外が捕捉され、それらの属性を持つ既存のレコードがfind_byを使用して発見されます。

これはfind_or_create_byに似ていますが、SELECTとINSERTの間の古い読み込みの問題を回避します。

しかし、create_or_find_byにはいくつかの欠点があります。

  • 基底となるテーブルは、一意の制約で関連する列を定義していなければなりません。
  • 一意な制約違反は、与えられた属性のうち1つだけ、または少なくともすべての属性よりも少ない属性によってトリガされる可能性があります。これは,後続のfind_byが一致するレコードの検索に失敗する可能性があることを意味し,与えられた属性を持つレコードではなく,ActiveRecord::RecordNotFoundの例外を発生させます.
  • find_or_create_byからSELECT -> INSERTの間の競合状態を回避していますが、実際にはINSERT -> SELECTの間に別の競合状態があり、これら2つのステートメントの間のDELETEが別のクライアントによって実行された場合にトリガーされます。しかし、ほとんどのアプリケーションでは、これはヒットする可能性がかなり低い条件です。
  • このメソッドは、制御フローを処理するために例外処理に依存しており、多少遅くなるかもしれません。

このメソッドは、すべての与えられた属性が一意の制約によってカバーされている場合(INSERT -> DELETE -> SELECTの競合条件がトリガされない限り)、レコードを返しますが、作成が試みられ、検証エラーのために失敗した場合、それは永続化されません、あなたはそのような状況でcreateが返すものを取得します。

https://github.com/rails/rails/blob/bca6f7f57693dc4244870f7bd7f37a4f5cbf1976/activerecord/lib/active_record/relation.rb#L178

というように、既存レコードが存在することが設定された値からユニーク成約で発覚するケースにおいては利用することができる。

参考リンク