某フリマサイトのコピーサイトを作っているときに直面した課題を解決できたので記事にします。
使用ファイルなど
- products_controller.rb
- products/index.html.haml
- productsテーブル
- categoriesテーブル
- brandsテーブル
前提条件
brandsテーブル
id | name |
---|---|
1 | ナイキ |
2 | ルイヴィトン |
3 | シュプリーム |
productsテーブル
id | name | brand_id |
---|---|---|
1 | キャップ | 1 |
2 | サッカーシューズ | 1 |
… | (省略) | … |
解決したい課題
・同じメソッドを何回も使いたいが、ビュー側では違う変数名で渡して区別したい
フリマサイトのトップページには
カテゴリ別
- レディース新着
- メンズ新着
- ベビー・キッズ新着
ブランド別
- ナイキ新着
- ルイヴィトン新着
- シュプリーム新着
などが4品ずつ表示されるようになっているので、
- 各カテゴリごとにやりたいことは同じ→同じメソッドを使いたい
- だけど中身はそれぞれ参照するカテゴリが違うため当然中身も変わってくる
- さらにビューで変数も変えないと区別して表示できない
ほう、困った
最初に思いついたこと
ブランドの方が簡単なのでブランドから取り組んでみました
class ProductsController < ApplicationController
def nike_products
@nikes = Product.where(brand_id: 1).order("id DESC").limit(4)
end
end
# ナイキがbrandsテーブルのid1番に登録されていると仮定
これで商品の中からブランドがナイキのものを取得できました。
でもこれって、ルイヴィトンでもシュプリームでもメソッド定義して、変数命名して、やるのかな?
ブランドが100個あったら100個定義するの?そんなはずは…
(執筆中さらに思いついたこと)
※2019/06/26編集済
class ProductsController < ApplicationController
def some_action_method
@nikes = brand_info(1)
@louisvuittons = brand_info(2)
@supremes = brand_info(3)
end
private
def brand_info(num)
Product.where(brand_id: num).order("id DESC").limit(4)
end
end
これならメソッド1つで実現はできます。
ですが、ブランドの数だけ変数が増えてしまいますよね。100個なら…?
結論:instance_variable_setメソッドで変数定義時に別の変数を組み込む
obj.instance_variable_set(name, val)
instance_variable_setメソッドは、レシーバのインスタンス変数に値を設定します。引数nameにはインスタンス変数の名前を:@titleや"@title"のようにシンボルか文字列で渡します。引数valには変数に設定する値を渡します。戻り値は設定した値です。
instance_variable_setメソッドはもともとインスタンス変数に値を設定するためのもの。
ただ、インスタンス変数名側にも別の変数を使えるという性質を利用します。
※2019/06/26編集済
1. class ProductsController < ApplicationController
2. def brand_info_set
3. (1..3).each do |num|
4. products = Product.where(brand_id: num).order("id DESC").limit(4)
5. instance_variable_set("@brand_no#{num}", products)
6. end
7. end
8. end
4行目でbrand_id: 1
の商品4つをproducts
に代入
5行目で先ほどのproducts
を@brand_no1
という変数に代入することに成功しました。
これを(1..3).each
で繰り返すため、
@brand_no1
,@brand_no2
,@brand_no3
というインスタンス変数を定義することができました。
中身はナイキ、ルイヴィトン、シュプリームがそれぞれ入っています。
これをview側に渡せば、ブランド分けも完璧ですね!
もっといい方法を教えてもらいました!
※2019/06/26追記
上記結論で書いたコードでは、トップページに表示したいブランドが変更した場合にコードを書き直さなければならなくなります。
class ProductsController < ApplicationController
def brand_info_set
Brand.all.each do |brand|
products = brand.products.order("id DESC").limit(4)
id = brand.id
instance_variable_set("@brand_no#{id}", products)
end
end
end
これならブランドテーブルに存在するものだけを変数化することができ、都度変数定義する必要がないためスッキリしました。
さらにスコープをモデルに書けば、
class Brand < ApplicationRecord
has_many :products
end
class Product < ApplicationRecord
scope :recent, -> { order("id DESC").limit(4) }
end
class ProductsController < ApplicationController
def brand_info_set
Brand.all.each do |brand|
products = brand.products.recent
id = brand.id
instance_variable_set("@brand_no#{id}", products)
end
end
end
order("id DESC").limit(4)」
を他でも使いたい場合にrecent
だけで済むのでもっとスッキリですね!
課題としては変数名にidを用いているため、直接的ではないという点でしょうか。
より良い案がありましたらコメントお願いします!
ちなみに
カテゴリも分けることができたのですが、ブランドの応用系であることと、
instance_variable_setの説明がメインの記事のため今回は割愛します。
参考記事
Rubyリファレンス:instance_variable_set (Object)
https://ref.xaio.jp/ruby/classes/object/instance_variable_set
[Ruby]Rubyの変数の名前に変数の中身を使う方法
http://portaltan.hatenablog.com/entry/2015/07/03/161157
Railsでよく利用する、Scopeの使い方。
https://qiita.com/ngron/items/14a39ce62c9d30bf3ac3