地方でリモートワーク in Iwate

東京の受託開発会社でリモートワークしてます。

Railsでgroup byを使って、0も考慮しながら合計値を出す方法

スポンサーリンク

f:id:ihatov08:20160712103332j:plain

Railsでgroup byを使って、0も考慮しながら合計値を出す方法です。

Viewでcountしていた

viewでカテゴリごとの商品数を表示していました。 productsとproduct_categoriesテーブルは別々に持っています。 はじめはviewでcountしていたんです。

<%= Product.where_selling.count %>
<% @parent_categories.each do |parent_category| %>
  <%= parent_category.name %>
  <%= content_tag :span, parent_category.products.where_selling.count %>
<% end %>

でもこれだと<%= content_tag :span, parent_category.products.where_selling.count %>の度にsqlが呼ばれてしまいます。

groupを使って合計値を取得する

これを解決するにはgroupメソッドを使います。 モデルにscopeを定義しました。 productテーブルを結合して、product_categoriesの名前でグルーピングしています。 今回はすべてのカテゴリを表示したかったので、left join使ってます! これでproductsテーブルに存在しないカテゴリも取得できます!

class ProductCategory < ActiveRecord::Base

  scope :get_parent_categories, -> {
    where('parent_id is null').joins("LEFT OUTER JOIN products ON product_categories.id = products.product_category_id").group('product_categories.name').order(:sort_order)
  }
end
[1] pry(main)> ProductCategory.get_parent_categories
  ProductCategory Load (56.1ms)  SELECT `product_categories`.* FROM `product_categories` LEFT OUTER JOIN products ON product_categories.id = products.product_category_id WHERE (parent_id is null) GROUP BY product_categories.name  ORDER BY `product_categories`.`sort_order` ASC

pluckで取得したいカラムを指定

pluckメソッドを使って、取得したいカラムを指定します。 今回はhelperに定義しました!

  def product_category_count(product_categories)
    product_categories.pluck('product_categories.id, product_categories.name, count(products.id)')
  end

これで配列の中にネストした配列でインスタンスごとに[カテゴリID, カテゴリ名, カテゴリごとの商品数]が格納されて返ってきます!

[5] pry(main)> ProductCategory.get_parent_categories.pluck('product_categories.id, product_categories.name, count(products.id)')
   (0.7ms)  SELECT product_categories.id, product_categories.name, count(products.id) FROM `product_categories` LEFT OUTER JOIN products ON product_categories.id = products.product_category_id WHERE (parent_id is null) GROUP BY product_categories.name  ORDER BY `product_categories`.`sort_order` ASC
=> [[1, "家電・スマホ", 1],
 [2, "家具・インテリア", 0],
 [3, "エンタメ", 1],
 [4, "スポーツ・アウトドア", 2],
 [5, "ファッション・ブランド", 2],
 [6, "コスメ・香水・美容", 0],
 [7, "自動車・オートバイ", 1],
 [8, "ライフスタイル", 0]]

viewで表示させる

そしてviewで表示させます。

=> [[1, "家電・スマホ", 1],
 [2, "家具・インテリア", 0],
 [3, "エンタメ", 1],
 [4, "スポーツ・アウトドア", 2],
 [5, "ファッション・ブランド", 2],
 [6, "コスメ・香水・美容", 0],
 [7, "自動車・オートバイ", 1],
 [8, "ライフスタイル", 0]]

このようにネストした配列の場合、ブロック変数をネストしている配列の要素数だけ用意してあげると1つのeach文で扱うことができます。

<% product_category_count(@parent_categories).each do |id, category_name, category_count| %>
    <li>
      <%= link_to products_path(cat: id) do %>
        <%= category_name %>
        <%= content_tag :span, category_count %>
      <% end %>
    </li>
<% end %>

商品数合計はどう表示する?

下記のような値が取得できているので、ネストされた配列の3番目の要素を全て足してあげれば、商品数合計を出せます。これでsqlが1文で取得したい値が全て取得できます!

=> [[1, "家電・スマホ", 1],
 [2, "家具・インテリア", 0],
 [3, "エンタメ", 1],
 [4, "スポーツ・アウトドア", 2],
 [5, "ファッション・ブランド", 2],
 [6, "コスメ・香水・美容", 0],
 [7, "自動車・オートバイ", 1],
 [8, "ライフスタイル", 0]]

helperにメソッドを作りました。

  def total_product_count(product_categories)
    product_categories.map{|m| m[2]}.inject(:+)
  end

mapを使って3番目の要素のみ取得しています。

[6] pry(main)> product_categories = ProductCategory.get_parent_categories.pluck('product_categories.id, product_categories.name, count(products.id)')
   (0.7ms)  SELECT product_categories.id, product_categories.name, count(products.id) FROM `product_categories` LEFT OUTER JOIN products ON product_categories.id = products.product_category_id WHERE (parent_id is null) GROUP BY product_categories.name  ORDER BY `product_categories`.`sort_order` ASC
=> [[1, "家電・スマホ", 1],
 [2, "家具・インテリア", 0],
 [3, "エンタメ", 1],
 [4, "スポーツ・アウトドア", 2],
 [5, "ファッション・ブランド", 2],
 [6, "コスメ・香水・美容", 0],
 [7, "自動車・オートバイ", 1],
 [8, "ライフスタイル", 0]]
[7] pry(main)> product_categories.map{|m| m[2]}
=> [1, 0, 1, 2, 2, 0, 1, 0]

そしてinjectメソッドで配列を全て足しています!

[8] pry(main)> count = [1, 0, 1, 2, 2, 0, 1, 0]
=> [1, 0, 1, 2, 2, 0, 1, 0]
[9] pry(main)> count.inject(:+)
=> 7

pry便利

pryで色々試しながらやると確認しながらできるので、いいですね!!