概要
某フリマサイトの商品カテゴリーのクローンを作成するときにお役立てください。
多段階カテゴリを実装する際、gem 'ancestry'というものがあることを知り、実装してみようと思ったら、レコードを投入する'seed.rb'に大量の記述が必要になりました。
記述ミスを防ぐためにも自分なりに、なんとか記述量を減らしたいなぁと方法を考えてみました。
答えだけ知りたい方は本記事STEP3をコピペすればOKです。
gem 'ancestry'を使うための準備
こちらは公式のGitHubを参考にしました。
Gemfile
Gemfileに下記を記載
gem 'ancestry'
Categoriesテーブルにanccestryを適用させるため
ターミナルで下記コマンドを実行
$ bundle install
$ rails g migration add_ancestry_to_category ancestry:string
マイグレーションファイルに下記を記載
class AddAncestryToCategory < ActiveRecord::Migration[5.2]
def change
add_column :categories, :ancestry, :string
add_index :categories, :ancestry
end
end
仕上げにmigrateをしてください
$ rake db:migrate
Model
class Category < ApplicationRecord
# 中略
has_ancestry
end
レコードを作成 (本記事の趣旨)
比較対象として
1. 一般的な書き方
2. 今回の実装した書き方
の2パターンを記載します
レコードの階層想定
3階層まであると想定して
- (第1階層) カテゴリ 1
- (第2階層) カテゴリ 1-1
- (第3階層) カテゴリ 1-1-1
- (第3階層) カテゴリ 1-1-2
- (第3階層) カテゴリ 1-1-3
- (第2階層) カテゴリ 1-2
- (第3階層) カテゴリ 1-2-1
- (第3階層) カテゴリ 1-2-2
- (第2階層) カテゴリ 1-1
- (第1階層) カテゴリ 2
- (第2階層) カテゴリ 2-1
- (第3階層) カテゴリ 2-1-1
- (第3階層) カテゴリ 2-1-2
- (第3階層) カテゴリ 2-1-3
- (第2階層) カテゴリ 2-2
- (第3階層) カテゴリ 2-2-1
- (第3階層) カテゴリ 2-2-2
- (第2階層) カテゴリ 2-1
とします
1. 一般的な書き方
category1 = Category.create(name:"カテゴリ 1")
category1_1 = category1.children.create(name:"カテゴリ 1-1")
category1_2 = category1.children.create(name:"カテゴリ 1-2")
category1_1.children.create([{name:"カテゴリ 1-1-1"},{name:"カテゴリ 1-1-2"},{name:"カテゴリ 1-1-3"}])
category1_2.children.create([{name:"カテゴリ 1-2-1"},{name:"カテゴリ 1-2-2"}])
#以後省略
想定ぐらいのレコードの数であれば、なんとかなりそうです。
しかし、今回のフリマアプリでは総レコードが1000以上。ムムム
2. 今回の実装した書き方
STEPを踏んで考えていきます。
STEP1 ~第2階層のみを考える~
まずは、第1階層の"カテゴリ 1"の直下に第2階層を作る方法を考えていきます
@category1 = Category.create(name:"カテゴリ 1")
category1s = ["カテゴリ 1-1","カテゴリ 1-2"]
category1s.each do |category-item|
@category1.children.create(name:"#{category-item}")
end
配列を使って、作成してました。
お気づきの方もいらっしゃるかもしれませんが、
category1ではなく**@category1**と第1階層を定義してます。
変数名に @ を付けるとスコープが広がり、その後のeachのブロック内でも@変数が使えます。
each内で@category1 = Category.create(name:"カテゴリ 1")を定義すればいいじゃんと思う方もいるかもしれませんが、のちのちメリットがありますので、今はこのまま進めます。
STEP2 ~第3階層が増えた場合を対応させる~
流れとしては2階層を作って3階層を作ります。
@category1 = Category.create(name:"カテゴリ 1")
category1s = [
{level2:"カテゴリ 1-1",level2_children:["1-1-1","1-1-2","1-1-3"]},
{level2:"カテゴリ 1-2",level2_children:["1-2-1","1-2-2","1-2-3"]}
]
category1s.each.with_index(1) do |category1,i|
level2_var="@category1_#{i}"
level2_val= @category1.children.create(name:"#{category1[:level2]}")
eval("#{level2_var} = level2_val")
category1[:level2_children].each do |level2_children_val|
eval("#{level2_var}.children.create(name:level2_children_val)")
end
end
一気にややこしくなりました。
説明します。
#中略
category1s = [
{level2:"カテゴリ 1-1",level2_children:["1-1-1","1-1-2","1-1-3"]},
{level2:"カテゴリ 1-2",level2_children:["1-2-1","1-2-2","1-2-3"]}
]
#中略
まずは、配列category1sに第2階層と第3階層をハッシュ定義しました。
#中略
category1s.each.with_index(1) do |category1,i|
level2_var="@category1_#{i}"
level2_val= @category1.children.create(name:"#{category1[:level2]}")
#中略
最初のeach.with_index部分を説明します。
第3階層を作成するために第2階層作成時に変数も作らねばなりません。
変数を'level2_var'とし、例によって、第3階層作成時にスコープを超える必要があるため**@をつけました。
i=1のとき、level2_var = @category1_1
i=2のとき、level2_var = @category1_2
が作成されるわけです。
値を'level2_val'とし、第1階層作成時1の@category1の子供であるcategory1sのそれぞれの変数category1**のハッシュのキーである'level2'を取り出しました。
i=1のとき、level2_val = @category1.children.create(name:"カテゴリ 1-1")
i=2のとき、level2_val = @category1.children.create(name:"カテゴリ 1-2")
が作成されるわけです。
#中略
eval("#{level2_var} = level2_val")
#中略
evalメソッドを使っています。今回の肝です。
eval メソッドは、Ruby の組み込みカーネルメソッドで与えられた文字列をそのままRubyのコードとして解釈して実行します。
つまり、動的な変数**'level2_var','level2_val'の値**を文字列として認識しRubyのコードとして実行してます。こうすることで、右辺左辺の変数が動的でも実行できるわけですね。
#中略
category1[:level2_children].each do |level2_children_val|
eval("#{level2_var}.children.create(name:level2_children_val)")
end
end
最後です。
第1階層作成時の**@category1の子供であるcategory1sのそれぞれの変数category1のハッシュのキーである'level2_children'を取り出しました。
そして、第2階層作成時の'level2_var'**の子供として取り出したキーの名前でレコードを作成してます。
ここはeach.with_indexを使う必要はないので
1番最初処理の時のとき、
@category1_1.children.create(name:"カテゴリ 1-1-1")
2番最初処理の時のとき、
@category1_1.children.create(name:"カテゴリ 1-1-2")
が作成されるわけです。
そして、第2階層作成にもどり、i=2となります。
その後またこの処理が動き、
@category1_2.children.create(name:"カテゴリ 1-2-1")
@category1_2.children.create(name:"カテゴリ 1-2-2")
が作成されます。
ここでもevalを使用して動的変数をRubyとして実行してます。
STEP3 ~第1階層が複数ある場合を考える~
categories=[
{level1:"カテゴリ 1",level1_children:[
{level2:"カテゴリ 1-1",level2_children:["カテゴリ 1-1-1","カテゴリ 1-1-2","カテゴリ 1-1-3"]},
{level2:"カテゴリ 1-2",level2_children:["カテゴリ 1-2-1","カテゴリ 1-2-2"]}
]
},
{level1:"カテゴリ 2",level1_children:[
{level2:"カテゴリ 2-1",level2_children:["カテゴリ 2-1-1","カテゴリ 2-1-2","カテゴリ 2-1-3"]},
{level2:"カテゴリ 2-2",level2_children:["カテゴリ 2-2-1","カテゴリ 2-2-2"]}
]
}
]
categories.each.with_index(1) do |category,i|
level1_var="@category#{i}" #1階層の変数("@category1"等)
level1_val= Category.create(name:"#{category[:level1]}") #1階層の値作成("カテゴリ 1"等)
eval("#{level1_var} = level1_val") #1階層の変数=1階層の値
category[:level1_children].each.with_index(1) do |level1_child,j|
level2_var="#{level1_var}_#{j}" #2階層の変数("@category1-1"等)
level2_val= eval("#{level1_var}.children.create(name:level1_child[:level2])") #2階層の値作成("カテゴリ 1-1"等)
eval("#{level2_var} = level2_val") #2階層の変数=2階層の値
level1_child[:level2_children].each do |level2_children_val|
eval("#{level2_var}.children.create(name:level2_children_val)") #3階層の値作成("カテゴリ 1-1-1"等)
end
end
end
STEP2と考え方は同じです。
categoriesのバリューを変更すれば、応用できます。
最後に
ターミナルで仕上げをしてください!
$ rake db:seed
##参考文献
Rubyで連番の変数を動的に作成