TECH::EXPERT【rails】ツリー構造(多階層構造)のカテゴリを作る【82日目】

【学習内容】


・階層型DBとは
・ancestryを使う
・seeds.rbの編集

【階層型DBとは】

DBは大きく3種類に分類されます。
①階層型データベース
②ネットワーク型データベース
③リレーショナル型データベース

今回は①について学習します。

①階層型データベース
名前の通りデータを階層型に格納する仕組みをもったDBです。

データはツリー構造で表し、ある1つのデータが他の複数のデータに対して、「親子の関係」を持っています。

末端にあるデータにアクセスするためのルートは1通りだけですが、例外的にデータは絶対に親子関係になるとは限らず、「多対1」の場合や「多対多」の場合もありえるので、同じデータがあちこちの親データに所属する形になります。

例として会社の組織図を想像するとイメージしやすいのですが、ある1人の社員が複数の部門の仕事を兼任していた場合、組織図上は複数の部門に所属することになりますが、その社員はもちろん1人しか存在しません。(DB上には複数存在するが、本当は1人だけという現象が起きるということ。)

このようにデメリットとして、階層構造は重複が起こりやすくなってしまうので、プログラムはデータ構造に強く依存することになります。

そもそもなぜDBを作るかというと、「データの整理・一元化」をするためです。

つまり、本来は正確にわかりやすく整理するためにDBは存在します。

データが重複してしまったら元も子もないですし、データ構造を意識しながらプログラムを書いたり、データ構造に変更があった場合はプログラマに大きな負荷をかけることに繋がります。

※(ネットワーク型データベースとリレーショナル型データベースについて今回は触れないので各自ググってください)

ここまでのことをまとめると、

①データの中身は同じだが、親の位置が異なることにより、重複するデータ所有が発生する
②階層型データベース構造が修正される場合、プログラムの変更も必要で大変

大きくこの2点の特徴があります。

【メルカリ開発で階層型DBを設計する】

メルカリにおいて「商品」に関連するたくさんの情報をDBにまとめるには、多階層DBにする必要があります。

具体的には、下記のように親カテゴリー > 子カテゴリー >孫カテゴリーみたいな複数の層に渡る関係性を持っている場合ですね。

【親カテゴリー】 メンズ
【子カテゴリー】 ジャケット/アウター
【孫カテゴリー】 テーラード/ジャケット

もちろん各カテゴリは複数存在するので、3世代に渡ってそれぞれの関係性は全て「多対多」になります。

テーブル同士が「多対多」のときは、「中間テーブル」を作成しなければいけませんが、困ったことにこのままだと結構な数の中間テーブルが必要になりそうです。

<困っていること>

・カテゴリで親子関係をつけて階層を作りたい

・クエリの長さを考えて同じテーブルにて扱いたい

・カテゴリが親にもなり得るし、子にもなり得える

リレーショナルデータベースで階層構造(一般的にはナイーブツリーと呼ばれるアンチパターン)を扱うにあたり、いくつかの解法が存在します。

・隣接リストモデル
・入れ子集合モデル
・経路列挙型モデル

僕たちのチームは「経路列挙モデル」を採用することにしました。

【経路列挙モデルとは】

対象ノードの先祖までの経路を属性として格納することで、木構造を表現するモデルです。

簡単にいうと、
・1つ1つのデータにたどり着くまでのルートをそれぞれ属性値として管理する。
・そのルートを管理する属性(カラム)は「path」。

【ancestry】

上記の経路列挙型モデルを実現するために必要なのが「ancestry」というGemです。

https://github.com/stefankroes/ancestry

今回は「item」と「category」という「多対多」の関係にある2つのテーブルを使って、経路を作っていきます。

①Gemfile関連

Gemfile

gem 'ancestry'

ターミナル

$ bundle install
$ rails g migration add_ancestry_to_category ancestry:string:index
$ rake db:migrate

model

class Item < ApplicationRecord
 belongs_to user, foreign_key: 'user_id'
 belongs_to :category
 #(中略
end

class Category < ApplicationRecord
 has_many :items
 has_ancestry
end

見ての通り中間テーブルに関する記載がごっそりなくなりモデルの中身がすっきりしました。

また、category(1) 対 item(多)の関係になっていることがわかります。

なんだかエラーを吐きそうな香りがプンプンしますが、このままでOKです。

②レコードを入れる

初期データを追加する方法は直接データベースにアクセスして追加することもできますが、今回はRailsで用意されている仕組みを使います。

それが、「seeds.rb」です。

今回データを追加するcategoryテーブルで、クラスで用意されているクラスメソッドであるcreateメソッドを使ってデータを追加していきます。

モデルクラス名.create(:カラム名1 => 値1, :カラム名2 => 値2, ...)

また、ancestryを導入すると新たに「childrenメソッド」が使用可能になります。

使い方は、「変数.chirdren」と記載することで子要素としてオブジェクトを扱うことが出来ます。


seeds.rb

lady = Category.create(:name=>"レディース")

lady_tops = lady.children.create(:name=>"トップス")

lady_jacket = lady.children.create(:name=>"ジャケット/アウター")

lady_jacket.children.create([{:name=>"テーラードジャケット"}, {:name=>"ノーカラージャケット"}, {:name=>"Gジャン/デニムジャケット"},{:name=>"その他"}])
・
・
・
・
・

こんな感じで1行ずつレコードに記入をしてもよいのですが、だいぶ退屈な作業ですし、階層が増えたときに変更するのが難しいので、ここはプログラムを組むことで解決します。


③seeds.rbを編集する


■親階層→子階層

@category1 = Category.create(name:"カテゴリ 1")

#配列に子階層を全て入れる

category1s = ["カテゴリ 1-1","カテゴリ 1-2"…”カテゴリ1-10”] 

#each文で回して親階層から子階層を作成する

category1s.each do |category-item|
@category1.children.create(name:"#{category-item}")
end

■子階層→孫階層が増えたときの対策

@category1 = Category.create(name:"カテゴリ 1")

#配列category1sに第2階層と第3階層をハッシュ定義する
#要は親階層に紐づいている「子階層」と「孫階層」を全て配列に入れる
#孫階層に関しては配列の中に配列が入っている状態です。

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

インデックスつきで1から始まるeach文と2つの変数を用意しました。

i=1のとき、level2_var = @category1_1
i=2のとき、level2_var = @category1_2

i=1のとき、level2_val = @category1.children.create(name:"カテゴリ 1-1")
i=2のとき、level2_val = @category1.children.create(name:"カテゴリ 1-2")

eval メソッドは、俗にいうメタプログラミング(黒魔術)の一種で、Ruby の組み込みカーネルメソッドで与えられた文字列をそのままRubyのコードとして解釈して実行します。

■参考 Rubyのevalメソッドの使い方
https://uxmilk.jp/25938

動的な変数'level2_var','level2_val'の値を文字列として認識しRubyのコードとして実行することで変数が動的でも実行できます。


しかし、現状のままでは親階層が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


最後にseedsをdbに反映させるのを忘れずに!

$ rake db:seed


この記事が気に入ったらサポートをしてみませんか?