6
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Railsの `find_or_create_by` と `create_with` について

6
Posted at

この記事は ポートアドベントカレンダー 20日目の記事です:christmas_tree:

概要

Railsにある find_or_create_bycreate_with というメソッドの利用シーンについて考えてみました。
どちらもDBにレコードを作成するメソッドです。

環境

  • Rails 8.1.1
  • Ruby 3.4.7

前提

こんな感じのテーブルとモデルです。

  • 下記コマンドで作成可能
    (マイグレーションの実行や、バリデーションの追加は手動で作業が必要)
    • bin/rails generate model user name:string! age:integer
db/schema.rb(抜粋)
  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
app/models/user.rb
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_bycreate_with を組み合わせて使う

APIの例にも記載されていますが、下記のように find_or_create_bycreate_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軸を大切にしていく必要があると思いました :relieved:

  • 開発プロセスの充実: RuboCopの導入、RSpec/Minitestの拡充
  • エンジニアの意識・能力向上: 可読性・メンテナンス性を意識したソースコード作成、知識を増やす

参考URL

6
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?