(初心者向け)activerecord-importの使い方

2019年10月22日追記

**Rails6ではバルクインサートが標準でサポートされました。**
なので、Rails6以上を使う場合はgemを使用する必要がありません。
詳しくは[Rails 6のB面に隠れている地味にうれしい機能たち（翻訳）](https://techracho.bpsinc.jp/hachi8833/2019_05_16/74562) - Active Recordのinsert_allをご参照ください。

追記ここまで

---

RailsでBULK INSERTをする時にややハマったので。
ActiveRecord::Relationって何？AcitveRecord::Baseって何？というあたりの人に向けて書いています。

#BULK INSERTとは
テーブルに複数のレコードを登録する時に、
1行1行INSERTすると効率が悪いので、まとめてINSERTすること。

詳細かつとてもわかりやすい説明、また基本的な使い方は以下をご参照ください。
[ActiveRecordで複数レコード、BULK INSERTする方法とパフォーマンスについて](https://qiita.com/xend/items/79184ded56158ea1b97a)

#RailsでBULK INSERTをするには
上の記事でも紹介されていますが、activerecord-importというgemがあるのでそれを使います。

```ruby:Gemfile
gem 'activerecord-import'
```
```
$ bundle install
```

> 使い方はとても簡単で、登録するレコード（model）のインスタンスをすべて配列にいれて、クラスメソッドのimportにそれを渡すだけです。

初学者的には簡単ではなかったので、恥を忍んで同じような方の救いになればと思い書いてます。
なので、もっといいやり方などあったらぜひ教えていただけると幸いです。

#テーブルを集計して別のテーブルにimportするには

例えば昨月の本のレビューと売り上げを集計してテーブルに保存したい場合。
（とあるテーブルのデータを別のテーブルにコピーしたい場合）

```ruby
book_sales = Book.joins(:reviews, :sales).where(some_conditions).select(some_columns).group("books.id")
SaleLastMonth.import book_sales
```

とすると、`ArgumentError: Invalid arguments!`が返ってきます。
そうはいっても`some_columns`で指定したカラムと`SaleLastMonth`のカラムはちゃんとあってますし…。
Gemが親切にエラーを出してくれているので、ソースを読んでみます。

```ruby:ruby/2.5.0/gems/activerecord-import-0.22.0/lib/activerecord-import/import.rb
if args.last.is_a?( Array ) && args.last.first.is_a?(ActiveRecord::Base)
...(中略)
  # supports array of hash objects
elsif args.last.is_a?( Array ) && args.last.first.is_a?(Hash)
...(中略)
  # supports empty array
elsif args.last.is_a?( Array ) && args.last.empty?
...(中略)
  # supports 2-element array and array
elsif args.size == 2 && args.first.is_a?( Array ) && args.last.is_a?( Array )
...(中略)
else
  raise ArgumentError, "Invalid arguments!"
end
```

ふむふむ。察するに`args`にレコードたちが入ってるんですね。
そこで`args.last.first.class`を見てみると、`ActiveRecord::Relation`。
受け付けてくれるのは`ActiveRecord::Base`。

…って、`ActiveRecord::Relation`と`ActiveRecord::Base`、どう違うの？？
というのは以下のページに詳しいですが、ようは

> ActiveRecord::Relationは

> * クエリを生成するための条件を持っておいて、必要に応じて適切なSQLクエリを生成・発行してくれる
> * その結果からオブジェクトを作って返したり保持したりしてくれる

> ActiveRecord::Baseは

> + レコード一行を取り扱うクラス

つまりこうなる。

```ruby
example1= Book.where(id: 1)
example1.is_a?(ActiveRecord::Base) # => false: ActiveRecord::Relation
example2 = Book.find_by_id(1)
example2.is_a?(ActiveRecord::Base) # => true: ActiveRecord::Base
```

[ActiveRecord::Relationとは一体なんなのか](https://doruby.jp/users/whale/entries/ActiveRecord--Relation%E3%81%A8%E3%81%AF%E4%B8%80%E4%BD%93%E3%81%AA%E3%82%93%E3%81%AA%E3%81%AE%E3%81%8B)
[ActiveRecord::RelationとActiveRecord::Baseの違いを説明してください #4](https://github.com/TECH-CAMP-GEEKS/ruby_on_rails/issues/4)
なお、findとwhereの理解が怪しい方には次のページがおすすめです。
[【Rails初心者必見！】ひたすら丁寧にデータ取得を説明(find, where)](https://qiita.com/mr-myself/items/cfb936dcf63d2c44d2f5)

# 完成形

`book_sales = Book.joins(:reviews, :sales).where(some_conditions).select(some_columns).group("books.id")`でとれるのは`ActiveRecord::Relation`なので、`SaleLastMonth.import`には`ActiveRecord::Base`を渡してあげる。
これは`each`すればいいだけ、とか思いきやカラム名が違うって怒られるので、最終的にはこうしました。

```ruby
row_book_sales = Book.joins(:reviews, :sales).where(some_conditions).select(some_columns).group("books.id")

book_sales = row_book_sales.map do |sales|
  sales_last_month = SalesLastMonth.new
  [:some, :columns, :name].each do |column|
    sales_last_month.send("#{column.to_s}=", sales.send(column))
  end
  sales_last_month
end
SalesLastMonth.import book_sales
```

Gemの使い方というよりも基礎がわかっていなくて大分躓いたので、
もっと基本を勉強しよう、と思いました。