社内ドキュメントに書いたものの転載。
前提
猫と猫の脚というモデルがあり、猫は脚が一本以上は必要というバリデーションがされているとき、
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 # 書いても意味がない。先にcatを作ろうとするため、下記の通り、RecordInvalidになる 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
する場合には意図した通りになりません。
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の方が先に実行されてしまうようでバリデーションを通過できなかった。