すごく助かったのでメモがてら
環境
$ rails -v Rails 7.1.2
factory_bot (6.4.4) activesupport (>= 5.0.0) factory_bot_rails (6.4.2) factory_bot (~> 6.4) railties (>= 5.0.0)
やり方について
今回の事例
下記のように1000レコードを作成するようなテストを書く場合を想定する。
RSpec.describe SoccerMember, type: :model do context "1000人のメンバーが作成されたとき" do let!(:team) { create(:soccer_team) } before do ## この部分で SoccerMember を1000レコード分作る ## end it "正しい順番でならんでいること" do ## 略 ## end end end
素直にcreate_listを使う場合と課題点
factoryBotにはcreate_listという機能があるので、素直に使うと以下のようになる。
RSpec.describe SoccerMember, type: :model do context "1000人のメンバーが作成されたとき" do let!(:team) { create(:soccer_team) } before do create_list(:soccer_member, 1000, soccer_team: team) end it "正しい順番でならんでいること" do ## 略 ## end end end
ただし、この場合内部的には SoccerMember.create
が1000回繰り返されてしまうため、SQLのINSERTが1000回実行することになるのでパフォーマンスとしてはよろしくはない。
大体案としての build_list
+ insert_all
やりたいことの内訳としては
- FactoryBotに定義したカラムの内容を生成してもらう
- それをRDB上にレコードとしてつくる
というような内訳になるので、1と2の作業を分離する。
1. カラムの内容生成
1の作業としてbuild_listでオブジェクトをつくる。
active_record_list = build_list(:soccer_member, 1000, soccer_team: team, created_at: Time.zone.now, updated_at: Time.zone.now)
2. RDB上にレコードを作成する
2の作業として、insert_all
で実行を行う。insert_allはActiveRecordのオブジェクトではなく、attributesのhashの配列がほしいので変換してあげる。
attributes_array = active_record_list.each_with_object([]) do |soccer_member , result_array| attribute_hash = soccer_member.attributes result_array.push(attribute_hash) end SoccerMember.insert_all(attributes_array)
もっと簡潔に書くと以下のようにも書ける
SoccerMember.insert_all(active_record_list.map(&:attributes))
注意点として、ActiveRecord独自のattributesを増やしているとそれも .attributes
で取り出すときに含まれてしまうので注意。その場合はhashから取り除く必要がある。
実行時間の比較
比較として下記のように実行してみる。
RSpec.describe SoccerMember, type: :model do context "1000人のメンバーが作成されたとき" do let!(:team) { create(:soccer_team) } before do start_time = Time.zone.now.to_f create_list(:soccer_member, 1000, soccer_team: team) end_time = Time.zone.now.to_f elapsed_time = (end_time - start_time) * 1000 puts "create_listのみ 生成時間: #{elapsed_time}ミリ秒" end it "正しい順番でならんでいること" do ## 略 ## end end context "1000人のメンバーが作成されたとき" do let!(:team) { create(:soccer_team) } before do start_time = Time.zone.now.to_f now_time = Time.zone.now # NOTE: 明示的にcreated_atとupdated_atを入れてあげないとnilになってしまってエラーになるので入れる active_record_list = build_list(:soccer_member, 1000, soccer_team: team, created_at: now_time, updated_at: now_time ) SoccerMember.insert_all(active_record_list.map(&:attributes)) end_time = Time.zone.now.to_f elapsed_time = (end_time - start_time) * 1000 puts "build_list + insert_all 生成時間: #{elapsed_time}ミリ秒" end it "正しい順番でならんでいること" do ## 略 ## end end end
そうなると結果は以下のようになり、だいぶ短縮された。
create_listのみ 生成時間: 2795.0141429901123ミリ秒 build_list + insert_all 生成時間: 140.3510570526123ミリ秒