0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Railsで配列の配列(2次元配列)を解消する方法

Last updated at Posted at 2023-08-08

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

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

配列の配列とは?

例えば、下記のケースです。

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で解消できるのか見ていきましょう。

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を持っているとします。

tags = [tag1, tag2, tag3]

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

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

[[post1, post2], [post3, post4], [post5, post6]]

上記のように、外側の配列がtags.mapによって作られ、内側の配列はtag.postsの結果です。

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

[post1, post2, post3, post4, post5, post6]

これが、配列の配列が生成される理由と、それをフラットな配列に変換する方法です。

パフォーマンスを考慮したコードに改善しよう!

先ほど紹介した、下記のex1コントローラーは、パフォーマンス的に良くない点が2箇所あります。

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

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
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について

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. それぞれのタグに関連付けられたポストを配列として取得し、最終的に1つの配列に結合します。
    発行される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 = 1ページに表示させるpostのみを返す。(例えば10件のpost)

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

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

ex2について

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

2つの比較

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

ex1のような非効率な処理を避けるために気をつけること

  1. データベースとのやり取りを最小限に
    可能な限り少ない数のクエリで必要な情報を取得しましょう。N+1問題は特に注意が必要です。

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

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

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

最後に

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

0
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?