社内ドキュメントに書いたものの転載。
前提
猫と猫の脚というモデルがあり、猫は脚が一本以上は必要というバリデーションがされているとき、
class Cat < ApplicationRecord
has_many :cat_legs, dependent: :destroy
validates :cat_legs, presence: true
end
class CatLeg < ApplicationRecord
belongs_to :cat
end
素直にFactoryを書くとこうだが、
FactoryBot.define do
factory :cat do
name { 'たま' }
age { 5 }
end
factory :cat_leg do
nail { true }
association :cat
end
end
これらは create
では使えない。
pry(main)> FactoryBot.create(:cat)
ActiveRecord::RecordInvalid: Validation failed: Cat legs can't be blank
from /usr/local/bundle/gems/activerecord-6.0.3.3/lib/active_record/validations.rb:80:in `raise_validation_error'
pry(main)> FactoryBot.create(:cat_leg)
ActiveRecord::RecordInvalid: Validation failed: Cat legs can't be blank
from /usr/local/bundle/gems/activerecord-6.0.3.3/lib/active_record/validations.rb:80:in `raise_validation_error'
CatとCatLegは同時に保存されないとバリデーションを通過できない。
ファクトリー関係なく保存する方法
肝は、CatLegの方はsaveせずにbuildだけにしておいて、saveはCatの方で一回でやるということ。
これで、単一のトランザクションでINSERTされる。
irb(main):006:0> c = Cat.new
=> #<Cat id: nil, created_at: nil, updated_at: nil>
irb(main):007:0> c.cat_legs.build
=> #<CatLeg id: nil, cat_id: nil, created_at: nil, updated_at: nil>
irb(main):008:0> c.cat_legs.build
=> #<CatLeg id: nil, cat_id: nil, created_at: nil, updated_at: nil>
irb(main):009:0> c.save
(0.1ms) begin transaction
Cat Create (28.5ms) INSERT INTO "cats" ("created_at", "updated_at") VALUES (?, ?) [["created_at", "2021-06-16 08:13:51.866786"], ["updated_at", "2021-06-16 08:13:51.866786"]]
CatLeg Create (9.5ms) INSERT INTO "cat_legs" ("cat_id", "created_at", "updated_at") VALUES (?, ?, ?) [["cat_id", 12], ["created_at", "2021-06-16 08:13:51.905388"], ["updated_at", "2021-06-16 08:13:51.905388"]]
CatLeg Create (0.3ms) INSERT INTO "cat_legs" ("cat_id", "created_at", "updated_at") VALUES (?, ?, ?) [["cat_id", 12], ["created_at", "2021-06-16 08:13:51.919341"], ["updated_at", "2021-06-16 08:13:51.919341"]]
(40.5ms) commit transaction
1行でやりたいならこういうのでもいい。
irb(main):012:0> c = Cat.new(cat_legs: [CatLeg.new, CatLeg.new])
=> #<Cat id: nil, created_at: nil, updated_at: nil>
irb(main):013:0> c.save!
(0.1ms) begin transaction
Cat Create (25.2ms) INSERT INTO "cats" ("created_at", "updated_at") VALUES (?, ?) [["created_at", "2021-06-16 08:26:17.387995"], ["updated_at", "2021-06-16 08:26:17.387995"]]
CatLeg Create (8.7ms) INSERT INTO "cat_legs" ("cat_id", "created_at", "updated_at") VALUES (?, ?, ?) [["cat_id", 14], ["created_at", "2021-06-16 08:26:17.427883"], ["updated_at", "2021-06-16 08:26:17.427883"]]
CatLeg Create (0.6ms) INSERT INTO "cat_legs" ("cat_id", "created_at", "updated_at") VALUES (?, ?, ?) [["cat_id", 14], ["created_at", "2021-06-16 08:26:17.440913"], ["updated_at", "2021-06-16 08:26:17.440913"]]
(57.2ms) commit transaction
=> true
Catモデルに accepts_nested_attributes_for :cat_legs
があれば、もちろん、cat_legs_attributes
で一発で渡せる。
irb(main):001:0> Cat.create!(cat_legs_attributes: [{}])
(1.2ms) SELECT sqlite_version(*)
(0.1ms) begin transaction
Cat Create (25.7ms) INSERT INTO "cats" ("created_at", "updated_at") VALUES (?, ?) [["created_at", "2021-06-16 08:02:22.525263"], ["updated_at", "2021-06-16 08:02:22.525263"]]
CatLeg Create (10.4ms) INSERT INTO "cat_legs" ("cat_id", "created_at", "updated_at") VALUES (?, ?, ?) [["cat_id", 10], ["created_at", "2021-06-16 08:02:22.561846"], ["updated_at", "2021-06-16 08:02:22.561846"]]
(35.8ms) commit transaction
=> #<Cat id: 10, created_at: "2021-06-16 08:02:22", updated_at: "2021-06-16 08:02:22">
解法
ストラテジーごとにやり方はいくつかある。上から推奨順に書いている。
注意
以下のやり方は create_list
する場合には意図した通りになりません。
github.com
cat_legは同じインスタンスが使いまわされてしまうので、最後のcatにしかcalt_legsが入っていないことになってしまいます。複数作りたい場合は、諦めて素直にcreateを複数実行しましょう。
cats = [
FactoryBot.create(:cat, tags: [FactoryBot.build(:cat_leg)]),
FactoryBot.create(:cat, tags: [FactoryBot.build(:cat_leg)])
]
createのパラメータとして子のアソシエーションも渡す
いろいろ検討したけど、いつものやり方で普通にできた。
FactoryBot.create(:cat, cat_legs: FactoryBot.build_list(:cat_leg, 4))
これはaccepts_nested_attributes_forの有無に拘らず、単一のトランザクションで処理される。
irb(main):003:0> FactoryBot.create(:cat, cat_legs: FactoryBot.build_list(:cat_leg, 4))
(0.1ms) begin transaction
Cat Create (34.5ms) INSERT INTO "cats" ("created_at", "updated_at") VALUES (?, ?) [["created_at", "2021-06-16 07:50:50.484299"], ["updated_at", "2021-06-16 07:50:50.484299"]]
CatLeg Create (14.6ms) INSERT INTO "cat_legs" ("cat_id", "created_at", "updated_at") VALUES (?, ?, ?) [["cat_id", 7], ["created_at", "2021-06-16 07:50:50.527524"], ["updated_at", "2021-06-16 07:50:50.527524"]]
CatLeg Create (0.1ms) INSERT INTO "cat_legs" ("cat_id", "created_at", "updated_at") VALUES (?, ?, ?) [["cat_id", 7], ["created_at", "2021-06-16 07:50:50.548179"], ["updated_at", "2021-06-16 07:50:50.548179"]]
CatLeg Create (0.3ms) INSERT INTO "cat_legs" ("cat_id", "created_at", "updated_at") VALUES (?, ?, ?) [["cat_id", 7], ["created_at", "2021-06-16 07:50:50.551906"], ["updated_at", "2021-06-16 07:50:50.551906"]]
CatLeg Create (0.2ms) INSERT INTO "cat_legs" ("cat_id", "created_at", "updated_at") VALUES (?, ?, ?) [["cat_id", 7], ["created_at", "2021-06-16 07:50:50.555688"], ["updated_at", "2021-06-16 07:50:50.555688"]]
(37.0ms) commit transaction
=> #<Cat id: 7, created_at: "2021-06-16 07:50:50", updated_at: "2021-06-16 07:50:50">
build
して明示的にsaveする
cat = FactoryBot.build(:cat, cat_legs: FactoryBot.build_list(:cat_leg, 4))
cat.save!
以下のように2行に分けてやるとcatの方で関連を認識できないのでバリデーションを通過できないので注意。
cat = FactoryBot.build(:cat)
FactoryBot.build_list(:cat_leg, 4, cat: cat)
cat.save!
よく使う場合はfactoryと同じファイルでこんなメソッドを定義してもいい。
def cat_with_legs_by_build(legs_count: 4)
cat = FactoryBot.build(:cat, cat_legs: FactoryBot.build_list(:cat_leg, legs_count))
cat.save!
cat
end
(accepts_nested_attributes_forの場合) attributes_for
を使う
accepts_nested_attributes_for
なら子の属性も同時に渡せるのでこうもやれる。やってることは上記の build
のやり方と同じなので、 build
の方がコードがシンプルになりやすいのでbuildの方がいいと思う。
cat = Cat.new(FactoryBot.attributes_for(:cat).merge(cat_legs_attributes: FactoryBot.attributes_for_list(:cat_leg, 4)))
cat.save!
(非推奨) :skip_validate
を使う
create(:cat, :skip_validate)
で保存はできるが、あとでvalidかどうかの確認をしないといけないので、上記のやり方でカバーできるのであれば使わない方がいい。
ダメだったやり方
has_many association のやり方で
def cat_with_legs(legs_count: 5)
FactoryBot.create(:cat) do |user|
FactoryBot.create_list(:cat_leg, legs_count, cat: cat)
end
end
こんな感じでいけるかと思ったけど、やはり親のsaveの方が先に実行されてしまうようでバリデーションを通過できなかった。