Railsで配列の配列（２次元配列）を解消する方法



私はRailsのメンターをしています。👨‍🏫
生徒さんからの質問で、自分が感覚的にやってたことに気がつく瞬間があります。

今回は、生徒さんからの質問にあった、
**「配列オブジェクトを返すつもりが、配列の配列になってて、エラーになります！解消したいです！」**
という内容について、お話しします。👨‍🎓

## 配列の配列とは？

例えば、下記のケースです。
```ruby:hoge_controller.rb
def index
   tags = Tag.where(name: params[:name]) 
   @posts = tags.map do |tag| # これが配列の配列になっている。
       tag.posts
   end
   @posts = Kaminari.paginate_array(@posts).page(params[:page])
end
```
`@posts`が配列の配列になっていますので、KaminariでNo methodのエラーになります。
データとしては下記みたいな感じ。
[[postA,postB],[postC,postD]]

**みなさん、今すぐ説明できますか？　　
一度考えてみてください。**



## 結論
flat_mapメソッドを利用すれば解消できます。
なぜ配列の配列になるのか、なぜflat_mapで解消できるのか見ていきましょう。
```ruby:hoge_controller.rb
def index
   tags = Tag.where(name: params[:name]) 
   @posts = tags.flat_map do |tag| # これで配列の配列が、フラットの配列になります。
       tag.posts
   end
   @posts = Kaminari.paginate_array(@posts).page(params[:page])
end
```
**※記事の最後に、パフォーマンスを考慮したコードを提示します。**
実務で使う場合は、記事の最後まで注意深く読んで、最善の方法を採用してください。

## 配列の配列の問題点
配列の配列になってしまっている理由は、tag.postsが配列を返すメソッドであるためです。

tags.mapを使用して各tagに対してtag.postsを実行すると、このtag.postsはPostモデルのオブジェクトの配列（例: [post1, post2, ...]）を返します。

そのため、tags.mapの結果はtagごとのPostの配列を要素とする配列となります。

例えば、3つのtagがあり、それぞれが2つのpostを持っているとします。

```ruby
tags = [tag1, tag2, tag3]
```
ここで、
tag1.postsが[post1, post2]、
tag2.postsが[post3, post4]、
tag3.postsが[post5, post6]、
を返すとします。

tags.map { |tag| tag.posts }を実行すると、
次のような結果になります。

```ruby
[[post1, post2], [post3, post4], [post5, post6]]
```
上記のように、外側の配列がtags.mapによって作られ、内側の配列はtag.postsの結果です。

これをフラットな配列にするために、flat_mapメソッドを使用できます。
これにより、次のような結果を得られます。

```ruby
[post1, post2, post3, post4, post5, post6]
```
これが、配列の配列が生成される理由と、それをフラットな配列に変換する方法です。

## パフォーマンスを考慮したコードに改善しよう！
先ほど紹介した、下記のex1コントローラーは、パフォーマンス的に良くない点が**２箇所**あります。

下記のex1とex2の違いを比べてみましょう。

```ruby:ex1_controller.rb
def index
   tags = Tag.where(name: params[:name]) 
   @posts = tags.flat_map do |tag|
       tag.posts
   end
   @posts = Kaminari.paginate_array(@posts).page(params[:page])
end
```

```ruby:ex2_controller.rb
def index
   @posts = Post.joins(:tag).where(tags: { name: params[:name] }).page(params[:page])
end
```

ex1とex2は、同じ`@posts`を返しますが、ex2の方がパフォーマンスが高いです。以下に、ex1が劣る主要な3つの理由を説明します。

### ex1がパフォーマンスで劣る、3つの原因
1. N+1問題
**ex1**のアプローチでは、まず**Tag**モデルから関連するレコードを取得し、次にそれぞれの**tag**に対して**tag.posts**でSQLクエリを発行する必要があります。これは複数のSQLクエリを発行することを意味し、N+1問題として知られるパフォーマンスの落とし穴に該当します。

2. データ変換のオーバーヘッド
**tag.posts**が配列（**ActiveRecord::Associations::CollectionProxy**オブジェクト）を返すため、取得したデータを配列に変換し、その後再び配列をページネーションするために変換する必要があります。この連続した変換プロセスにより、不要なオーバーヘッドが発生しています。

3. メモリの使用量
**ex1**の方法では、`@posts`で全てのポストデータを取得します。しかしながら、ページネーションで表示されるデータは `@posts` の一部のみです。そのため、必要なデータのみを取得する**ex2**の方法の方が、メモリ消費量の面で効率的です。

### 処理の違いを見てみよう

#### ex1について
```ruby:ex1_controller.rb
def index
   tags = Tag.where(name: params[:name])  # ①
   @posts = tags.flat_map do |tag|  # ②
       tag.posts
   end
   @posts = Kaminari.paginate_array(@posts).page(params[:page])  # ③
end
```
1. 指定した名前のTagを全て取得します。
例）
**発行されるSQL：**　SELECT * FROM tags WHERE name = '指定した名前';
**取得できるデータ：** tags　= [”りんご”,”ブドウ”,”いちご”,]　

2. それぞれのタグに関連付けられたポストを配列として取得し、最終的に１つの配列に結合します。
**発行されるSQL：** 
（SELECT * FROM posts WHERE tag_id = りんごのID;
 SELECT * FROM posts WHERE tag_id = ぶどうのID;
 SELECT * FROM posts WHERE tag_id = いちごのID;）
**取得できるデータ：** `@posts`= [”りんご”,”ブドウ”,”いちご”,]　に関するpostデータ（例えば100件のpost）

3. 取得したポストの配列をページネーションのためにKaminari.paginate_arrayを使用して処理します。
**取得できるデータ：** `@posts`　= １ページに表示させるpostのみを返す。（例えば10件のpost）

* ②の部分で、分かる通り、tagsの分だけ、SQLが発行されいます。（N+1問題）
* postsで100件データを取得しているが、ページネーションで１０個のポスト/ページ　であれば、90個のデータ取得が無駄になる。
* 計算途中のtagsや`@posts`でメモリや計算処理を無駄に消費しています。

続いて、ex2の処理を見ていきましょう。

#### ex2について
```ruby:ex2_controller.rb
def index
   @posts = Post.joins(:tag).where(tags: { name: params[:name] }).page(params[:page])  # ①
end
```
1. １つのSQLで、必要なポストのみを取得します。
例）
**発行されるSQL：**　
SELECT posts.* FROM posts 
JOIN tags ON posts.tag_id = tags.id 
WHERE tags.name = '指定した名前' 
LIMIT x OFFSET y;
**取得できるデータ：** `@posts`　= １ページに表示させるpostのみを返す。（例えば10件のpost）

#### 2つの比較
一目瞭然だと思いますが、ex2の方がパフォーマンスに優れていることが分かるでしょう。
ex2では、JOINを使用してPostとTagを結合し、1回のSQLクエリで必要な情報を取得します。これにより、データベースへのアクセス回数が大幅に削減され、パフォーマンスが向上させています。

### ex1のような非効率な処理を避けるために気をつけること
1. データベースとのやり取りを最小限に
可能な限り少ない数のクエリで必要な情報を取得しましょう。N+1問題は特に注意が必要です。

2. ORMの特性を理解する
使用しているORM（ActiveRecord）の特性や提供しているメソッドをよく理解し、最も効率的なメソッドを使用しましょう。

3. ページネーション
大量のデータを取得する必要がある場合は、最初からページネーションを考慮してクエリを作成しましょう。これにより、不要なデータの取得やメモリの浪費を避けることができます。

4. 適切なデータベースの設計
効率的なクエリを書くためには、適切なデータベースの設計が基盤となります。正規化、インデックスの設定など、データベースの設計段階での選択がクエリの効率に大きく影響します。

## 最後に

今回はこれで以上です。
説明が間違っている、わかりにくければ、コメント頂ければ嬉しいです！！