【Rails】メガ級の大量データをseedで入れたいとき

# はじめに
[以前書いた記事](https://qiita.com/koki_73/items/2ebee06bf16c416aefa8)で100万件のレコードを用意したことがあったのですが、せっかくなのでその方法を紹介します。

ついでにちょっとした検証もしたのでそれも紹介します

# 結論はよ
* 大量データの作成は`activerecord_import`を使おう
* 処理の時間を測ってみたらかなりの差が出たよ

中級者以上のかたにとっては当たり前という内容かもなので、「あ、そういう内容ね」とここで想像できた方はこのタイミングでブラウザバックしてもらって構いません笑


# やり方

### Gem

先に使うgemを紹介します
```ruby:Gemfile
gem 'activerecord-import'
gem 'seed-fu'
```

`activerecord-import`というgemは大量データを作成する際に使われることが多く、簡単に説明すると作成したい大量のレコードを元に1つのSQL文を作成して、それを実行してくれるものです

詳しい使い方などはここでは解説しませんが、興味のある方は調べてみると良いでしょう。
一応githubのリンクを載せておきます

https://github.com/zdennis/activerecord-import

`seed-fu`というgemはseedの実行に使うもので、本記事の本質とは直接関係ないですが、あとで出てくるseed実行のコマンドに必要なので載せています

### seedファイル

seedファイルは以下のものを準備します
（当たり前ですが事前にpostsテーブルとpostモデルの作成をお忘れなく）

```ruby:db/fixtures/large_data_test/post.rb.rb
1000.times do |batch_idx|
  posts = []
  1000.times do |each_idx|
    posts << Post.new(title: "#{batch_idx + 1}_#{each_idx + 1}_test", content: 'test')
  end
  Post.import! posts
end
```

100万件のレコードを準備するのに1000件の一括インサート処理を1000回実行するイメージです。

メモリが許せば1万件の一括インサート処理を100回でも、100万件のインサート処理を1回でも良いです。
（インサート処理は少ない方が高速ですが、そのためのインサート文を作るのためのメモリが足りないと実行できないので、バランスを見ながらとなります。1000件ぐらいならメモリが枯渇することはほぼないと思われますし、seedならそこまで高速処理が求められないと思うので今回はこのようにしてます。）


ここまでできたら下記のコマンドを実行します
```bash
RAILS_ENV=development bundle exec rails db:seed_fu FILTER=post FIXTURE_PATH=./db/fixtures/large_data_test
```

これで100万件のレコードが用意できます

# 計測（おまけ）

100万件となると、100万timesを使う場合と比べて実行時間が大きく違うと思ったので計測してみました


```ruby:db/fixtures/large_data_test/post.rb.rb
def times_seed
  1_000_000.times do |i|
    Post.seed do |s|
      s.title = "title_#{i + 1}"
      s.content = 'sample'
    end
  end
end

def bulk_import
  1000.times do |batch_idx|
    posts = []
    1000.times do |each_idx|
      posts << Post.new(title: "#{batch_idx + 1}_#{each_idx + 1}_test", content: 'test')
    end
    Post.import! posts
  end
end

Benchmark.bm 10 do |r|
  r.report 'use times seed' do
    times_seed
  end

  r.report 'use activerecord import' do
    bulk_import
  end
end
```

これを実際に試すと次のような結果になりました。



```bash
$ RAILS_ENV=development bundle exec rails db:seed_fu FILTER=post FIXTURE_PATH=./db/fixtures/large_data_test             [~/projects/rails7_sample]!+[main]

== Filtering seed files against regexp: /post/

== Seed from ./db/fixtures/large_data_test/post.rb.rb
                 user     system      total        real
use times seed - Post {:title=>"title_1", :content=>"sample"}
 - Post {:title=>"title_2", :content=>"sample"}
 - Post {:title=>"title_3", :content=>"sample"}
 - Post {:title=>"title_4", :content=>"sample"}
...
...
...
 - Post {:title=>"title_999998", :content=>"sample"}
 - Post {:title=>"title_999999", :content=>"sample"}
 - Post {:title=>"title_1000000", :content=>"sample"}
1043.111819  75.961859 1119.073678 (8733.553886) 
use activerecord import 64.518842   0.569162  65.088004 ( 87.468840)  
```

実際には100万timesの処理をすると100万行のログが出力されるので、かなり見づらい出力結果になってしまいます（そもそも表示行数を100万行以上にしてないと最初から最後まで見れません笑）


少し分かりやすいように表にしてみます
| 方法 | 処理時間 /s |
|:-|:-:|
| times  | 1119.073678  |
| activerecord-import  | 65.088004  |

だいぶ実行時間に差が出ましたね〜

今回使ったPCはM1 Proでしたが、times使うと20分近くかかりました。
スペック低いPCでこの実験するとCPUが熱々になる可能性もあるのでもし試したい場合は件数少なめから様子見してやるようにしてください笑

大量データを用意する時は`activerecord_import`を使うことも検討してみてはいかがでしょう？（複雑な検索のクエリをテストしたい時など、もしかしたら活躍の場面があるかもしれません）