create_or_find_byの注意点
もはや暑い以外の言葉が見つからずクーラーという反転術式を覚えたい今日この頃です。
さて本日はRails 5.1.5から導入されてたと言われているcreate_or_find_by
を用いる際の注意点をシェアハピしていきたいと思っています。(ほぼコードコメントに書かれていることです...)
create_or_find_by
とは
元々あったfind_or_create_by
では、ほぼ同時にリクエストがあった場合にSELECT->INSERTを行うためvalidationエラーになってしまう問題がありました。
コードコメントにも書かれています。
そこで追加されたのが今回取り上げているcreate_or_find_by
です。
INSERT->SELECTで実行されるためほぼ同時にリクエストがあった場合に先にINSERTが実行された行をSELECTするようになっています。
実装的にはcreate()
を実行した後にActiveRecord::RecordNotUnique
をrescueしています。
注意点
先にざっと自分が感じたcreate_or_find_by
を使う際の注意点を挙げます。
- DBレベルでUNIQUE制約をつけておく
-
ActiveRecord::RecordNotFound
のハンドリングを考慮する - rescueによるハンドリングをしているため若干重い
- INSERT->SELECTのコンフリクトの可能性
- modelのvalidationをかけている場合は上手く動かないことがある
さてそれぞれの注意点を解説していきます!!
DBレベルでUNIQUE制約をつけておく
これが一番重要でコードコメントにもしっかり書かれています。
ActiveRecord::RecordNotUnique
をrescueするとfind_by
が実行される実装になっているためですね。
実際にUNIQUE制約をつけた場合とつけなかった場合にそれぞれどのようになるか見てみましょう。
UNIQUE制約をつけなかった場合
create_table "hoges", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t|
t.string "uuid", null: false
t.string "name", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["uuid"], name: "index_hoges_on_uuid"
end
# sandoxモード使っています
b23bf9d416ff:/app# ./bin/rails c -s
Any modifications you make will be rolled back on exit
shogun(dev)> uuid = SecureRandom.uuid
=> "9f507919-ada5-4a80-9932-6f733d3563c8"
shogun(dev)* hoge1 = Hoge.create_or_find_by(uuid: uuid) do |h|
shogun(dev)* h.name = 'fuga'
shogun(dev)> end
TRANSACTION (0.1ms) BEGIN
TRANSACTION (0.2ms) SAVEPOINT active_record_1
Hoge Create (0.4ms) INSERT INTO `hoges` (`uuid`, `name`, `created_at`, `updated_at`) VALUES ('9f507919-ada5-4a80-9932-6f733d3563c8', 'fuga', '2025-08-01 04:19:53.635865', '2025-08-01 04:19:53.635865')
TRANSACTION (0.1ms) RELEASE SAVEPOINT active_record_1
=> #<Hoge:0x0000ffff7de44bc0 id: 1, uuid: "9f507919-ada5-4a80-9932-6f733d3563c8", name: "fuga", created_at: "2025-08-01 13:19:53.635865000 +0900", updated_at: "2025-08-01 13:19:53.635865000 +0900">
shogun(dev)* hoge2 = Hoge.create_or_find_by(uuid: uuid) do |h|
shogun(dev)* h.name = 'piyo'
shogun(dev)> end
TRANSACTION (0.6ms) SAVEPOINT active_record_1
TRANSACTION (10.2ms) SAVEPOINT active_record_2
Hoge Create (30.8ms) INSERT INTO `hoges` (`uuid`, `name`, `created_at`, `updated_at`) VALUES ('9f507919-ada5-4a80-9932-6f733d3563c8', 'piyo', '2025-08-01 04:20:15.619954', '2025-08-01 04:20:15.619954')
TRANSACTION (0.1ms) RELEASE SAVEPOINT active_record_2
=> #<Hoge:0x0000ffff7da56400 id: 2, uuid: "9f507919-ada5-4a80-9932-6f733d3563c8", name: "piyo", created_at: "2025-08-01 13:20:15.619954000 +0900", updated_at: "2025-08-01 13:20:15.619954000 +0900">
shogun(dev)> Hogeg.all
(shogun):10:in '<main>': uninitialized constant Hogeg (NameError)
shogun(dev)> Hoge.all
TRANSACTION (0.5ms) SAVEPOINT active_record_2
Hoge Load (1.3ms) SELECT `hoges`.* FROM `hoges` /* loading for pp */ LIMIT 11
=>
[#<Hoge:0x0000ffff7fd5c940 id: 1, uuid: "9f507919-ada5-4a80-9932-6f733d3563c8", name: "fuga", created_at: "2025-08-01 13:19:53.635865000 +0900", updated_at: "2025-08-01 13:19:53.635865000 +0900">,
#<Hoge:0x0000ffff7fd5c800 id: 2, uuid: "9f507919-ada5-4a80-9932-6f733d3563c8", name: "piyo", created_at: "2025-08-01 13:20:15.619954000 +0900", updated_at: "2025-08-01 13:20:15.619954000 +0900">]
UNIQUE制約をつけていない場合はActiveRecord::RecordNotUnique
がraiseされないため2行できてしまいますね。
find_or_create_by
の場合は渡されたattributesでSELECTを実行するためUNIQUE制約がない場合でも1行になります!!
UNIQUE制約をつけた場合
create_table "hoges", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t|
t.string "uuid", null: false
t.string "name", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["uuid"], name: "index_hoges_on_uuid", unique: true
end
b23bf9d416ff:/app# ./bin/rails c -s
Any modifications you make will be rolled back on exit
shogun(dev)> uuid = SecureRandom.uuid
=> "69f8b394-9de6-426b-a432-9b0eb7e59616"
shogun(dev)* hoge1 = Hoge.create_or_find_by(uuid: uuid) do |h|
shogun(dev)* h.name = 'fuga'
shogun(dev)> end
TRANSACTION (0.2ms) BEGIN
TRANSACTION (0.2ms) SAVEPOINT active_record_1
Hoge Create (0.5ms) INSERT INTO `hoges` (`uuid`, `name`, `created_at`, `updated_at`) VALUES ('69f8b394-9de6-426b-a432-9b0eb7e59616', 'fuga', '2025-08-01 04:25:01.810012', '2025-08-01 04:25:01.810012')
TRANSACTION (0.2ms) RELEASE SAVEPOINT active_record_1
=> #<Hoge:0x0000ffff97a49a38 id: 4, uuid: "69f8b394-9de6-426b-a432-9b0eb7e59616", name: "fuga", created_at: "2025-08-01 13:25:01.810012000 +0900", updated_at: "2025-08-01 13:25:01.810012000 +0900">
shogun(dev)* hoge2 = Hoge.create_or_find_by(uuid: uuid) do |h|
shogun(dev)* h.name = 'piyo'
shogun(dev)> end
TRANSACTION (0.3ms) SAVEPOINT active_record_1
TRANSACTION (7.4ms) SAVEPOINT active_record_2
Hoge Create (25.3ms) INSERT INTO `hoges` (`uuid`, `name`, `created_at`, `updated_at`) VALUES ('69f8b394-9de6-426b-a432-9b0eb7e59616', 'piyo', '2025-08-01 04:25:15.762897', '2025-08-01 04:25:15.762897')
TRANSACTION (0.1ms) ROLLBACK TO SAVEPOINT active_record_2
Hoge Load (0.5ms) SELECT `hoges`.* FROM `hoges` WHERE `hoges`.`uuid` = '69f8b394-9de6-426b-a432-9b0eb7e59616' AND `hoges`.`uuid` = '69f8b394-9de6-426b-a432-9b0eb7e59616' LIMIT 1 FOR UPDATE
=> #<Hoge:0x0000ffff996a6e10 id: 4, uuid: "69f8b394-9de6-426b-a432-9b0eb7e59616", name: "fuga", created_at: "2025-08-01 13:25:01.810012000 +0900", updated_at: "2025-08-01 13:25:01.810012000 +0900">
shogun(dev)> Hoge.all
TRANSACTION (0.4ms) SAVEPOINT active_record_2
Hoge Load (1.0ms) SELECT `hoges`.* FROM `hoges` /* loading for pp */ LIMIT 11
=> [#<Hoge:0x0000ffff996a0290 id: 4, uuid: "69f8b394-9de6-426b-a432-9b0eb7e59616", name: "fuga", created_at: "2025-08-01 13:25:01.810012000 +0900", updated_at: "2025-08-01 13:25:01.810012000 +0900">]
UNIQUE制約をつけるとINSERTを実行した後ロールバックを行いSELECTを実行していることが分かりますね。
以下のようにcreate_or_find_by
に渡すattributesが複数の場合は複合UNIQUE制約をつける必要があるので注意してください。
Hoge.create_or_find_by(uuid: uuid, name: 'fuga') # この場合はuuidとnameの複合UNIQUE制約にする必要がある
ActiveRecord::RecordNotFound
のハンドリングを考慮する
失敗時に例外をraiseして欲しい際には!
をつけたcreate_or_find_by!
を使うと思います。
find_or_create_by!
の場合は実装が以下になっています。
ここではfind_by!
ではないためActiveRecord::RecordNotFound
の例外を呼び出し側で考慮する必要はありません。(実装上当たり前)
create_or_find_by!
の場合の実装は以下です。
INSERT->ROLLBACK->SELECTなのでfind_by!
が呼ばれています。
find_by!
だとActiveRecord::RecordNotFound
がraiseされる可能性があります。
create_or_find_by!
を呼ぶ際にはActiveRecord::RecordNotFound
がraiseされる可能性も考慮し呼び出しましょう!!
rescueによるハンドリングをしているため若干重い
これはコードコメントにも書かれています。
スペックにもよりますがfind_or_create_by
とどのくらい差分があるか見てみましょう。
benchmarkを使いたいので以下の関数を用意しました。
def find_or_create_by_uuid(uuid, name)
Hoge.find_or_create_by(uuid: uuid) do |h|
h.name = name
end
end
def create_or_find_by_uuid(uuid, name)
Hoge.create_or_find_by(uuid: uuid) do |h|
h.name = name
end
end
find_or_create_by
の場合
b23bf9d416ff:/app# ./bin/rails c
shogun(dev)> uuid = SecureRandom.uuid
=> "edabce2c-d8f8-436f-81e8-7cdf965df4f7"
shogun(dev)> Hoge.create(uuid: uuid, name: 'fuga')
=> #<Hoge:0x0000ffff7d6162a0 id: 8, uuid: "edabce2c-d8f8-436f-81e8-7cdf965df4f7", name: "fuga", created_at: "2025-08-01 13:51:21.850880000 +0900", updated_at: "2025-08-01 13:51:21.850880000 +0900">
shogun(dev)* time = Benchmark.realtime do
shogun(dev)* Hoge.find_or_create_by_uuid(uuid, 'piyo')
shogun(dev)> end
shogun(dev)> puts "実行時間: #{time * 1000}ms"
実行時間: 7.614292000653222ms
実行時間は7.6msでした。
create_or_find_by
の場合
b23bf9d416ff:/app# ./bin/rails c -s
shogun(dev)> uuid = SecureRandom.uuid
=> "bfcfb985-8759-4ccb-ac7e-f9d91458d626"
shogun(dev)> Hoge.create(uuid: uuid, name: 'fuga')
=> #<Hoge:0x0000ffff8de03598 id: 9, uuid: "bfcfb985-8759-4ccb-ac7e-f9d91458d626", name: "fuga", created_at: "2025-08-01 13:52:56.587854000 +0900", updated_at: "2025-08-01 13:52:56.587854000 +0900">
shogun(dev)* time = Benchmark.realtime do
shogun(dev)* Hoge.create_or_find_by_uuid(uuid, 'piyo')
shogun(dev)> end
shogun(dev)> puts "実行時間: #{time * 1000}ms"
実行時間: 12.433457995939534ms
実行時間は12.4msでした。
やはりcreate_or_find_by
の方が若干遅いっぽいですね。とはいえ手元のスペックでは5ms程度の差でした。
INSERT->SELECTのコンフリクトの可能性
こちらもコードコメントにもあります。
リクエスト1とリクエスト2が同時に来た場合を想定すると以下のような状態でコンフリクトが起きそうです。
とはいえ図にある通り以下を満たさないと再現はしなさそうです。
- 同時にリクエストが来る
-
create_or_find_by
の直後にdestroyをする - destoryの後に後続リクエストでfind_byが走る
modelのvalidationをかけている場合は上手く動かないことがある
以下のようにmodelにもUNIQUE制約のvalidationをかけているとします。
class Hoge < ApplicationRecord
validates :uuid, presence: true, uniqueness: true
validates :name, presence: true
end
するとcreate_or_find_by!
を使うと以下のようになります。
b23bf9d416ff:/app# ./bin/rails c
shogun(dev)> uuid = SecureRandom.uuid
=> "69c346c6-2c56-49fe-a61b-b65fbeb9f330"
shogun(dev)> Hoge.create(uuid: uuid, name: 'fuga')
=> #<Hoge:0x0000ffff5bbe6fd0 id: 12, uuid: "69c346c6-2c56-49fe-a61b-b65fbeb9f330", name: "fuga", created_at: "2025-08-01 16:38:52.840131000 +0900", updated_at: "2025-08-01 16:38:52.840131000 +0900">
shogun(dev)* hoge1 = Hoge.create_or_find_by!(uuid: uuid) do |h|
shogun(dev)* h.name = 'fuga'
shogun(dev)> end
(shogun):3:in '<main>': Validation failed: Uuid has already been taken (ActiveRecord::RecordInvalid)
modelのvalidationで落ちるのでActiveRecord::RecordInvalid
がraiseされるためですね...
ちなみにcreate_or_find_by
であれば問題なかったです。
b23bf9d416ff:/app# ./bin/rails c
shogun(dev)> uuid = SecureRandom.uuid
=> "6c0d48aa-50e1-455d-bbc9-f20064219753"
shogun(dev)> Hoge.create(uuid: uuid, name: 'fuga')
=> #<Hoge:0x0000ffff70d52120 id: 11, uuid: "6c0d48aa-50e1-455d-bbc9-f20064219753", name: "fuga", created_at: "2025-08-01 16:37:05.796180000 +0900", updated_at: "2025-08-01 16:37:05.796180000 +0900">
shogun(dev)* hoge1 = Hoge.create_or_find_by(uuid: uuid) do |h|
shogun(dev)* h.name = 'fuga'
shogun(dev)> end
=> #<Hoge:0x0000ffff70888360 id: nil, uuid: "6c0d48aa-50e1-455d-bbc9-f20064219753", name: "fuga", created_at: nil, updated_at: nil>
shogun(dev)> Hoge.all
TRANSACTION (0.3ms) SAVEPOINT active_record_5
Hoge Load (0.9ms) SELECT `hoges`.* FROM `hoges` /* loading for pp */ LIMIT 11
=>
[#<Hoge:0x0000ffff72432598
id: 11,
uuid: "6c0d48aa-50e1-455d-bbc9-f20064219753",
name: "fuga",
created_at: "2025-08-01 16:37:05.796180000 +0900",
updated_at: "2025-08-01 16:37:05.796180000 +0900">]
これは内部的に呼ばれているmethodがcreate
だからな気がしますね。
とはいえcreate_or_find_by!
が使いたいケースもあると思います、その際はstrict
が使えます。
class Hoge < ApplicationRecord
validates :uuid, presence: true, uniqueness: true, strict: ActiveRecord::RecordNotUnique
validates :name, presence: true
end
b23bf9d416ff:/app# ./bin/rails c
shogun(dev)> uuid = SecureRandom.uuid
=> "e82a96df-e525-4c99-888b-eeccf9e20287"
shogun(dev)> Hoge.create(uuid: uuid, name: 'fuga')
=> #<Hoge:0x0000ffff83f4e4c0 id: 13, uuid: "e82a96df-e525-4c99-888b-eeccf9e20287", name: "fuga", created_at: "2025-08-01 16:44:32.665228000 +0900", updated_at: "2025-08-01 16:44:32.665228000 +0900">
shogun(dev)* hoge1 = Hoge.create_or_find_by!(uuid: uuid) do |h|
shogun(dev)* h.name = 'fuga'
shogun(dev)> end
=> #<Hoge:0x0000ffff8287a640 id: 13, uuid: "e82a96df-e525-4c99-888b-eeccf9e20287", name: "fuga", created_at: "2025-08-01 16:44:32.665228000 +0900", updated_at: "2025-08-01 16:44:32.665228000 +0900">
とはいえstrict
でRailsのデフォルトの挙動を変えてしまう部分もあるため、利用するときはよく影響を考える必要がありそうですね。
まとめ
本記事はcreate_or_find_by
という関数を使う際の注意点についての記事でした。
注意点としては以下です。
- DBレベルでUNIQUE制約をつけておく
-
ActiveRecord::RecordNotFound
のハンドリングを考慮する - rescueによるハンドリングをしているため若干重い
- INSERT->SELECTのコンフリクトの可能性
- modelのvalidationをかけている場合は上手く動かないことがある
色々と注意点はありつつも既存のfind_or_create_by
で起きるSELECT->INSERTのコンフリクトを回避できるのはでかいですね。
用法用量を守っていいRailsライフを過ごしてください!!
Discussion