この記事は ポートアドベントカレンダー 20日目の記事です![]()
概要
Railsにある find_or_create_by と create_with というメソッドの利用シーンについて考えてみました。
どちらもDBにレコードを作成するメソッドです。
環境
- Rails 8.1.1
- Ruby 3.4.7
前提
こんな感じのテーブルとモデルです。
- 下記コマンドで作成可能
(マイグレーションの実行や、バリデーションの追加は手動で作業が必要)bin/rails generate model user name:string! age:integer
create_table "users", force: :cascade do |t|
t.string "name", null: false
t.integer "age"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
class User < ApplicationRecord
validates :name, presence: true
end
本題
find_or_create_by とは
「DBにレコードがあれば取得するだけ。なければ作る」という動きをするメソッドです。
# name: 'Alexandra' のレコードを作成
User.create(name: 'Alexandra', age: 10)
# => #<User id: 1, name: "Alexandra", age: 10,...>
# レコードが取得されるだけ。作成されない (name: 'Alexandra' のレコードがすでにあるため)
User.find_or_create_by(name: 'Alexandra')
# => #<User id: 1, name: "Alexandra", age: 10,...>
# レコードが作成される (name: 'Brian' のレコードがないため)
User.find_or_create_by(name: 'Brian')
# => #<User id: 2, name: "Brian", age: nil,...>
ブロックを使って、レコードを作成する場合の要素の値を指定することができます。
レコードがすでにある場合は、ブロック内の記載は無視されます。(ブロック内に書いた値での更新などはされない)
# name: 'Alexandra' のレコードを作成
User.create(name: 'Alexandra', age: 10)
# => #<User id: 1, name: "Alexandra", age: 10,...>
# レコードが取得されるだけ。作成されない (name: 'Alexandra' のレコードがすでにあるため)
# ブロックの中の記載は無視される。ageは更新されない
User.find_or_create_by(name: 'Alexandra') do |user|
user.age = 15 # 新しくレコードを作る場合だけ参照される
end
# => #<User id: 1, name: "Alexandra", age: 10,...>
# レコードが作成される (name: 'Brian' のレコードがないため)
# age にはブロック内で指定した 20 が登録される
User.find_or_create_by(name: 'Brian') do |user|
user.age = 20 # 新しくレコードを作る場合だけ参照される
end
# => #<User id: 2, name: "Brian", age: 20,...>
create_with とは
レコードを新しく作成するときの初期値を設定することができます。
users = User.create_with(age: 0)
# age が 0 になる
users.create(name: 'Taro')
# => #<User id: 1, name: "Taro", age: 0,...>
users.create(name: 'Jiro')
# => #<User id: 2, name: "Jiro", age: 0,...>
いつ使うと便利か
find_or_create_by を使う
find_or_create_by は説明するまでもなく、「レコードを探して、なかったら作る」の時に便利です。
メソッド名からして意図も伝わりやすいですね。
「なかったら作る」という処理の本題に集中できるところもいいです。
# find_or_create_by を使わない
User.create(name: 'Alexandra', age: 10) if User.where(name: 'Alexandra').empty?
# find_or_create_by を使う
User.find_or_create_by(name: 'Alexandra') do |user|
user.age = 10
end
create_with を使う
create_with は、「デフォルトはこの値にしたい」という時に便利です。
(DBのデフォルト値、モデルのAttribute設定、insert_all など色々ありますが、今回は一旦おいておく)
下記の例だと、 create_with を使った方が「age: 0 はいつも固定」という意図が伝わりやすいです。
# create_with を使わない
User.create(name: 'Taro', age: 0)
User.create(name: 'Jiro', age: 0)
User.create(name: 'Hanako', age: 0)
# create_with を使う
users = User.create_with(age: 0)
names = %w[Taro Jiro Hanako]
names.each { |name| users.create(name:) }
find_or_create_by と create_with を組み合わせて使う
APIの例にも記載されていますが、下記のように find_or_create_by と create_with を組み合わせることもできます。
# ブロックを使う場合
User.find_or_create_by(name: 'Alexandra') do |user|
user.age = 10
end
# => #<User id: 1, name: "Alexandra", age: 10,...>
# create_with を使う場合
User.create_with(age: 10).find_or_create_by(name: 'Alexandra')
# => #<User id: 1, name: "Alexandra", age: 10,...>
おわりに
Railsは、同じ処理を書く際にいろいろな方法を選択できるようになっています。
(今回のようなレコード作成でも、 build create update save find_or_initialize_by...など何でも使える)
基本的なことではありますが、
その時・そのプロジェクトにとって最もベターなソースコードが書けるように、下記の2軸を大切にしていく必要があると思いました ![]()
- 開発プロセスの充実: RuboCopの導入、RSpec/Minitestの拡充
- エンジニアの意識・能力向上: 可読性・メンテナンス性を意識したソースコード作成、知識を増やす