LoginSignup
12
11

More than 3 years have passed since last update.

多階層カテゴリ作成でgem 'ancestry'を使用する際、seed.rbの記述量を大幅に減らす

Last updated at Posted at 2019-09-11

概要

某フリマサイトの商品カテゴリーのクローンを作成するときにお役立てください。
多段階カテゴリを実装する際、gem 'ancestry'というものがあることを知り、実装してみようと思ったら、レコードを投入する'seed.rb'に大量の記述が必要になりました。
記述ミスを防ぐためにも自分なりに、なんとか記述量を減らしたいなぁと方法を考えてみました。
答えだけ知りたい方は本記事STEP3をコピペすればOKです。
Image from Gyazo

gem 'ancestry'を使うための準備

こちらは公式のGitHubを参考にしました。

Gemfile

Gemfileに下記を記載

gem 'ancestry'

Categoriesテーブルにanccestryを適用させるため
ターミナルで下記コマンドを実行

$ bundle install
$ rails g migration add_ancestry_to_category ancestry:string

マイグレーションファイルに下記を記載

migration.rb
class AddAncestryToCategory < ActiveRecord::Migration[5.2]
  def change
    add_column :categories, :ancestry, :string
    add_index :categories, :ancestry
  end
end

仕上げにmigrateをしてください

$ rake db:migrate

Model

category.rb
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
  • (第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

とします

1. 一般的な書き方

seed.rb
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階層を作る方法を考えていきます

seed.rb

@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階層を作ります。

seed.rb

@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

一気にややこしくなりました。
説明します。

seed.rb
#中略
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階層をハッシュ定義しました。

seed.rb
#中略
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")
が作成されるわけです。

seed.rb
#中略
  eval("#{level2_var} = level2_val")
#中略

evalメソッドを使っています。今回の肝です。
eval メソッドは、Ruby の組み込みカーネルメソッドで与えられた文字列をそのままRubyのコードとして解釈して実行します。
つまり、動的な変数'level2_var','level2_val'を文字列として認識しRubyのコードとして実行してます。こうすることで、右辺左辺の変数が動的でも実行できるわけですね。

seed.rb
#中略
  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階層が複数ある場合を考える~

seed.rb
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で連番の変数を動的に作成

12
11
0

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
12
11