【Rails】 ancestryを使って多階層のデータを扱おう
【Rails】使用 ancestry 來處理多層次的數據

ancestryは簡単に多階層のデータを扱うことができるgemです。便利なメソッドも豊富に用意されています。
ancestry 是一個可以輕鬆處理多層次數據的 gem。它提供了豐富的方便方法。
データを登録するフォームを作る時、多階層(親子関係)で選択したい時はありませんか?
您在製作登錄數據的表單時,是否有時想要在多層次(父子關係)中進行選擇?
例えば、趣味を選択するとき、大カテゴリーで「映画鑑賞」や「スポーツ観戦」、「読書」などをまず選択します。
例如,當選擇興趣時,首先在大類別中選擇「觀賞電影」、「觀看體育比賽」、「閱讀」等。
その中で中カテゴリーで「映画鑑賞」であれば「アクション」とか「コメディ」とか「サスペンス」を選択する場合、それぞれのカテゴリーは親と子の関係になりますね。
在這些中,如果在中類別中選擇「電影觀賞」,則「動作」、「喜劇」或「懸疑」等,每個類別都會成為父子關係。
そんな時ひとつのテーブルで階層を分けてデータを管理するときに便利なのがancestry
というgemです。
在這種情況下,一個很方便的 gem 是 ancestry
,可以在一個表中分層管理數據。
データの親子関係を理解しよう 讓我們了解數據之間的親子關係
そもそもデータの多階層とは何なのでしょうか? 從根本上來說,多層次數據是什麼?
簡単に言うと親子関係です。 簡單來說就是親子關係。
例えば楽天市場の「ジャンル」をみてみましょう。 讓我們來看看樂天市場的「類別」。
ここでは一番左のジャンル一覧が「親」、吹き出しで表示されている左の部分が「子」、右が「孫」という関係を表します。
這裡最左邊的類別列表是「父」,在氣泡中顯示的左邊部分是「子」,右邊是「孫」,表示它們之間的關係。
このように一番上が「親」、その下に「子」、その下に「孫」という3階層になります。
這樣最上層是「親」,其下是「子」,再下是「孫」,形成了三層結構。
このような構造を多階層構造と言います。 這種結構被稱為多層結構。
gemをインストールしよう 安裝 gem
それではancestryを使う準備をしていきましょう。
讓我們開始準備使用 ancestry。
Gemfileに下記のコードを追記しましょう。 讓我們在 Gemfile 中添加以下代碼。
1
gem 'ancestry'
その後、bundle installコマンドを実行します。
然後,執行 bundle install 命令。
ancestryの使い方 祖先的使用方式
この章ではancestryの具体的な使い方を解説していきます。
在這一章中,我們將解釋 ancestry 的具體用法。
多階層のテーブルを作成しよう 建立多層次的表格
今回は最初に紹介した楽天の市場のジャンルを例にして説明をしていきます。
這次將以最初介紹的樂天市場的類別作為例子進行解釋。
まずはジャンルを管理するテーブルを作成します。 首先創建一個管理類型的表。
ターミナルで下記のrails g modelコマンドを実行し、genreモデルを作成します。
在終端機上運行以下的 rails g model 命令,創建 genre 模型。
1
$ rails g model Genre
このコマンドによりモデルを定義したgenre.rbとgenresテーブルを作成するマイグレーションファイルなどが作成されます。
通過這個命令,將定義模型的 genre.rb 和創建 genres 表的遷移文件等將被創建。
作成されたマイグレーションファイルを下記のように編集してください。
請編輯已創建的遷移文件如下所示。
db/migrate/バージョン名_create_genres.rb | genres 表創建
1
2
3
4
5
6
7
8
9
class CreateGenres < ActiveRecord::Migration[6.0]
def change
create_table :genres do |t|
t.string :name, null: false
t.string :ancestry
t.timestamps
end
end
end
ancestryを使う場合は上のようにancestryカラム
が必要となるのでancestryカラムの追記を必ずしましょう。
使用 ancestry 時,需要像上面那樣添加 ancestryカラム
,所以請務必記得追加 ancestry 列。
その後、ターミナルで下記のコマンドを実行し、genresテーブルを作成します。
之後,在終端機上執行以下命令,創建 genres 表。
1
$ rails db:migrate
モデルを編集しよう 編輯模型
次にモデルを編集します。 接下來編輯模型。
ancestryを有効にするためにはモデルにhas_ancestry
を記述する必要があります。
為了啟用 ancestry,您需要在模型中記錄 has_ancestry
。
appフォルダ内のmodelsフォルダにあるgenre.rb
を下記のように編集してください。
請將位於 app 資料夾內的 models 資料夾中的 genre.rb
進行如下編輯。
app/models/genre.rb | 代碼的添加
1
2
3
class Genre < ApplicationRecord
has_ancestry #このコードを追記
end
次に作成したテーブルにジャンルの名前をセットします。
接下來將類別名稱設置到創建的表中。
この時いちいちジャンルの投稿フォームを作ってcreateアクションで保存したり、コンソールからひとつずつ入力していくというのはとても手間がかかります。
這時候一一創建類別的投稿表單,並在 create 操作中保存,或者從控制台逐個輸入,這樣非常費時費力。
テーブルにデータを一気に作成したい場合はseedファイルを作成すると簡単にデータをセットすることができます。
如果您想要一次性在表格中創建數據,可以創建種子文件,這樣就可以輕鬆設置數據。
seedファイルを作成してデータをセットしよう
創建種子檔案並設置數據
まずはseedファイルを作成します。 首先創建 seed 文件。
dbフォルダにあるseeds.rb
を下記のように編集します。
將位於 db 資料夾中的 seeds.rb
進行以下編輯。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
drink = Genre.create(name: 'ドリンク・お酒')
water, beer, sake, wine = drink.children.create(
[
{ name: '水・ソフトドリンク' },
{ name: 'ビール・洋酒' },
{ name: '日本酒・焼酎' },
{ name: 'ワイン' }
]
)
['水・ミネラルウォーター', 'コーヒー', '野菜・果実飲料', 'お茶・紅茶', '炭酸飲料', 'スポーツドリンク'].each do |name|
water.children.create(name: name)
end
['ビール・発泡酒', 'ウイスキー', 'チューハイ・ハイボール・カクテル'].each do |name|
beer.children.create(name: name)
end
%w[焼酎 日本酒 梅酒].each do |name|
sake.children.create(name: name)
end
['赤ワイン', '白ワイン', '飲み比べセット', 'スパークリングワイン・シャンパン'].each do |name|
wine.children.create(name: name)
end
上記コード内のchildren
メソッドはレシーバー(メソッドを使ったインスタンス)の子のレコードを作成してくれるancestryで用意されているメソッドです。
上述代碼中的 children
方法是一個由 ancestry 提供的方法,用於創建接收器(使用該方法的實例)的子記錄。
その後、ターミナルで下記のコマンドを実行するとgenresテーブルにデータがセットされます。
之後,在終端機上執行以下命令,將數據設置到 genres 表中。
1
$ rails db:seed
sequel proなどでデータがセットされてるか確認してみましょう。
讓我們在 sequel pro 等工具中檢查數據是否已設置。
このようにデータがセットされていれば大丈夫です。
如果數據設置正確,就沒問題。
ancestryカラムの見方 祖先列的查看方式
一番上の親(ルート)に当たるレコードはancestryカラムがnullになるのがポイントです。
一番上的父母(根)對應的記錄是祖先列為空的關鍵。
その子に当たるレコードはancestryカラムが親レコードのidと同じ1
となります。
該記錄的子記錄將具有與父記錄的 id 相同的祖先列 1
。
ルートレコードは複数つくることもできます。 路由記錄也可以創建多個。
もしancestryカラムがnullのルートレコードのidが6であれば、id6の子レコードとするときはancestryカラムには6
が入ります。
如果 ancestry 列為 null 的根記錄的 id 為 6,則將其視為 id6 的子記錄,此時 ancestry 列將填入 6
。
そして孫カラムは1/2
のように/
で区分けされるのがポイントです。
然後,孫卡倫被區分為 1/2
是重點。
1/2
の1
の部分はこのレコードの一番上の親のidを指します。
1/2
的 1
部分指的是這條記錄的最上層父級 id。
2
の部分は直近の親レコードのidを指します。
2
的部分指的是最近的父記錄的 id。
つまりancestryカラムに1/2
という値が入っているレコードは一番上の親のidが1
、その下の親のidは2
ということになります。
換句話說,當祖先欄位中包含值 1/2
的記錄時,最上層父母的 id 為 1
,其下一層父母的 id 為 2
。
1/2/18
となっていれば一番上の親のidは1
、下の子レコードのidが2
、その下の孫レコードのidが18
となります。
1/2/18
如果是,最上層父級的 id 將是 1
,下層子記錄的 id 將是 2
,其下孫記錄的 id 將是 18
。
このようにancestryではidの数値を「 / 」で区切って親子関係を表現しています。
這樣在 ancestry 中,通過用「/」分隔 id 數值來表示父子關係。
もう一度先程作成したデータの中身を確認してみましょう。
讓我們再次檢查剛才創建的數據內容。
例えばidが7
の「コーヒー」という名前のジャンルがあります。
例如,有一個 ID 為 7
的名為「咖啡」的類別。
このレコードの親子関係はどうなっているか確認してみましょう。
讓我們來確認這個記錄的親子關係。
このancestryカラムの値は1/2
となっています。
這個 ancestry 欄位的值為 1/2
。
つまり一番上の親のidが1
なので、「ドリンク・日本酒」のジャンルであることがわかります。
換句話說,最上層父母的 id 是 1
,因此可以知道它屬於「飲料・清酒」這個類別。
さらに1/2
とあるのでその下の子はidが2
の「水・ソフトドリンク」であることがわかります。
此外,由於 1/2
,因此可以知道下面的子項目的 id 是 2
,並且是“水・軟飲料”。
まとめると「ドリンク・日本酒」 > 「水・ソフトドリンク」 > 「コーヒー」
という親子関係になります。
もう一つ例を見てみましょう。
idが18
の「赤ワイン」という名前のジャンルがあります。
このレコードの親子関係はどうなっているか確認してみましょう。
このancestryカラムの値は1/5
となっています。
つまり一番上の親のidが1
なので、「ドリンク・日本酒」のジャンルであることがわかります。
さらに1/5
とあるのでその下の子はidが5
の「ワイン」であることがわかります。
まとめると「ドリンク・日本酒」 > 「ワイン」 > 「赤ワイン」
という親子関係になります。
更に子のレコードを作成してみよう
いまだと親・子・孫という3世代の関係です。
さらにひ孫を作成するとancestryカラムはどうなるでしょうか?
試しに上で例にあげたidが18の赤ワインのさらに子のレコードを作成してみます。
1
2
3
4
5
6
7
8
9
irb(main):001:0>red_wine = Genre.find(18)
Genre Load (0.6ms) SELECT `genres`.* FROM `genres` WHERE `genres`.`id` = 18 LIMIT 1
=> #<Genre id: 18, name: "赤ワイン", ancestry: "1/5", created_at: "XXXX-XX-XX XX:XX:XX", updated_at: "XXXX-XX-XX XX:XX:XX">
irb(main):002:0> bordeaux = red_wine.children.create(name: "ボルドー")
(0.4ms) BEGIN
Genre Create (0.7ms) INSERT INTO `genres` (`name`, `ancestry`, `created_at`, `updated_at`) VALUES ('ボルドー', '1/5/18', 'XXXX-XX-XX XX:XX:XX', 'XXXX-XX-XX XX:XX:XX')
(1.2ms) COMMIT
=> #<Genre id: 22, name: "ボルドー", ancestry: "1/5/18", created_at: "XXXX-XX-XX XX:XX:XX", updated_at: "XXXX-XX-XX XX:XX:XX">
bordeaux = red_wine.children.create(name: "ボルドー")
というようにレコードに対して子レコードを作成するchildren
メソッドを使用します。
すると1/5/18
のようにidが18の赤ワインの子レコードとして登録されました。
今回はancestryで多階層のデータを扱いましたが、そもそもデータベースの知識について不安があるという方は、こちらの書籍を読むと良いでしょう。
便利なメソッドを使おう
ancestryにはseedファイルに出てきたchildren
メソッドなど、親子関係のデータを簡単に取得できる便利なインスタンスメソッドが定義されています。
メソッド名 | 返り値 |
---|---|
parent | 親レコードを取得する |
parent_id | 親レコードのidを取得する |
root | 一番上の親レコードを取得する |
root_id | 一番上の親レコードのidを取得する |
root? is_root? |
レコードが一番上の親ならtrueが返る |
ancestors | 自分の一番上の親から直近の上の親までのレコードを取得する |
ancestors_ids | 自分の親レコードのidを全て取得する |
path | 自分の一番上の親から自分で終わるレコード全てを取得する |
path_ids | 自分の一番上の親から自分で終わるレコードのidを全てを取得する |
children | 自分の子のレコードを全て取得する |
child_ids | 自分の子のレコードのidを全て取得する |
has_parent? ancestors? |
自分の親レコードが存在すればtrueが返る |
has_children? children? |
自分の子レコードが存在すればtrueが返る |
is_childless? childless? |
自分の子レコードが存在しなければtrueが返る |
siblings | 自分と同じ階層のレコードを全て取得する |
sibling_ids | 自分と同じ階層のレコードのidを全て取得する |
has_siblings? siblings? |
自分の親が1つ以上のレコードを持っていればtrueが返る |
is_only_child? only_child? |
自分が親レコードの唯一の子レコードであればtrueが返る |
descendants | 自分の子レコード以降の全ての階層のレコードを取得する |
descendant_ids | 自分の子レコード以降の全ての階層のレコードをのid取得する |
indirects | 自分の孫レコード以下のレコードを全て取得する |
indirect_ids | 自分の孫レコード以下のレコードのidを全て取得する |
subtree | 自分のレコードと子レコード以下全てのレコードを取得する |
subtree_ids | 自分のレコードと子レコード以下全てのレコードのidを取得する |
depth | 自分のレコードの上にどれくらいの階層があるのかを返す |
ではそれぞれのメソッドの詳細を確認していきます。
解説内の図の中の枠が青の丸はレシーバー(メソッドを使った自身)を、背景が黄色の丸は取得するレコードを指します。
parentメソッド
自分の親レコードを取得します。
自分の親はひとつなので原則1つのレコードを取得します。
自分の親が存在しない場合はnilが返ります。
下の例Aだと「ビール・発泡酒」のレコードがidが12
のレコードです。
このレコードの親はancestryカラムを確認すると「1/3」
となっているのでidが3
のレコードが親、idが1
のレコードが親の親ということがわかります。
parentメソッドは自分の直前の親レコードを取得するのでidが3
のレコードが取得されるということになります。
以降の図もこの例を参考にしながらイメージしてみてください。
▪️例A
▪️例B
parent_idメソッド
parentメソッドで取得したレコードのidを取得します。
自分の親が存在しない場合はnilが返ります。
上の例だと一番上のルートレコードに使っているため、親が存在せずnilが返ります。
rootメソッド
一番上の親レコード(ルート)を取得します。
一番上の親はひとつなので原則1つのレコードを取得します。
ルートのレコードに使用すると自分自身を返します。
root_idメソッド
rootメソッドで取得したレコードのidを取得します。
root? / is_root?メソッド
レコードが一番上の親レコード(ルート)であればtrue
を返すメソッドです。
rootメソッドの画像のAのレコードがルートレコードなので、Aのレコードに対して使えばtrue
が、それ以外のレコードだとfalse
が返ります。
2つのメソッドは同じ返り値を返すので、どちらを使ってもOKです。
ancestorsメソッド
自分の一番上の親(ルート)から直近の上の親までのレコードを取得するメソッドです。
ancestor_idsメソッド
自分の親レコード全てのidを取得するメソッドです。
ancestorsメソッドの画像の例だと黄色のレコードのidを全て取得します。
pathメソッド
自分の一番上の親(ルート)から自分で終わるレコード全てを取得します。
ancestorsメソッドとは違い自分のレコードも取得します。
path_idsメソッド
上のpathメソッドで取得したレコードのidを全て取得します。
childrenメソッド
自分の子のレコードを全て取得するメソッドです。
child_idsメソッド
自分の子のレコードのidを全て取得するメソッドです。
childrenメソッドで取得できるレコードのidを全て取得します。
has_parent? / ancestors?メソッド
自分の親レコードが存在すればtrue
が返るメソッドです。
下の図だと「ドリンク・お酒」のレコードに使った場合は親レコードが存在しないのでfalse
が返ります。
逆にその他のレコードはそれぞれ親が存在するのでtrue
が返ります。
2つのメソッドは同じ返り値を返すので、どちらを使ってもOKです。
has_children? / children?メソッド
自分の子レコードが存在すればtrue
が返るメソッドです。
2つのメソッドは同じ返り値を返すので、どちらを使ってもOKです。
下の図だと「ドリンク・お酒」から「ワイン」のレコードに使った場合は子レコードが存在するのでtrue
が返ります。
逆に「水・ミネラルウォーター」から「スパークリングワイン・シャンパン」のレコードは子が存在しないのでfalse
が返ります。
is_childless? / childless?メソッド
上のメソッドと逆の結果が返ってくるメソッドです。
先程の図だと「ドリンク・お酒」から「ワイン」のレコードに使った場合は子レコードが存在するのでfalse
が返ります。
逆に「水・ミネラルウォーター」から「スパークリングワイン・シャンパン」のレコードは子が存在しないのでtrue
が返ります。
2つのメソッドは同じ返り値を返すので、どちらを使ってもOKです。
siblingsメソッド
自分と同じ階層のレコードを全て取得するメソッドです。
自分も含めて全てを取得します。
sibling_idsメソッド
自分と同じ階層のレコードのidを全て取得するメソッドです。
has_siblings? / siblings?メソッド
自分の親が1つ以上の子レコードを持っていたらtrue
が返るメソッドです。
下の図の場合、「ビール・洋酒」の親は「ドリンク・お酒」です。
「ドリンク・お酒」は1つ以上の子レコードを持っているのでtrueが返ります。
2つのメソッドは同じ返り値を返すので、どちらを使ってもOKです。
is_only_child? / only_child?メソッド
自分が親レコードの唯一の子レコードであればtrue
が返るメソッドです。
2つのメソッドは同じ返り値を返すので、どちらを使ってもOKです。
上の図の場合、E、H、I、J、K、Lがtrue
を返します。
descendantsメソッド
自分の子レコード以降の全ての階層のレコードを取得するメソッドです。
上の例だと途中で「...」と省略されていますが、lengthメソッドを使うと全ての子レコードが取得できているのが確認できます。
descendant_idsメソッド
自分の子レコード以降の全ての階層のレコードのidを取得するメソッドです。
descendantsメソッドで取得できるレコードのidを取得します。
indirectsメソッド
自分の孫レコード以下のレコードを全て取得するメソッドです。
上の例だと途中で「...」と省略されていますが、lengthメソッドを使うと全ての孫レコードが取得できているのが確認できます。
indirect_idsメソッド
自分の孫レコード以下のレコードのidを全て取得するメソッドです。
indirectsメソッドで取得できるレコードのidを取得します。
subtreeメソッド
自分のレコードと子レコード以下全てのレコードを取得するメソッドです。
上の例だと途中で「...」と省略されていますが、lengthメソッドを使うと全てのレコードが取得できているのが確認できます。
subtree_idsメソッド
自分のレコードと子レコード以下全てのレコードのidを取得するメソッドです。
subtreeメソッドで取得できるレコードのidを取得します。
depthメソッド
自分のレコードの上にどれくらいの階層があるのかを返すメソッドです。
「ドリンク・お酒」の場合はルートなので0
が返ります。
「水・ソフトドリンク」の場合は上に「ドリンク・お酒」の階層があるので1
が返ります。
「ビール・発泡酒」の場合は上に「ドリンク・お酒」と「ビール・洋酒」の階層があるので2
が返ります。
Lの場合は上にA、D、Hと3つの階層があるので3
という数値が返ります。
Aの場合はルートなので0
が返ります。
Bの場合は1
が返ります。
便利なクラスメソッドを使おう
ancestryにはモデルクラスが使えるメソッドも用意されています。
メソッド名 | 返り値 |
---|---|
roots | ルートレコードを全て取得する |
ancestors_of(node) | 引数で渡したレコードの一番上の親から直近の上の親までのレコードを全て取得する |
children_of(node) | 引数で渡したレコードの子レコードを全て取得する |
descendants_of(node) | 引数で渡したレコード以降の全ての階層のレコードを取得する |
indirects_of(node) | 引数で渡したレコードの孫レコード以下のレコードを全て取得する |
subtree_of(node) | 引数で渡したレコードと子レコード以下全てのレコードを取得する |
siblings_of(node) | 引数で渡したレコードと同じ階層のレコードを全て取得する |
ではそれぞれのメソッドの詳細を確認していきます。
図の中の枠が青の丸は引数で渡したレコードを、背景が黄色の丸は取得するレコードを指します。
rootsメソッド
ルートレコード(一番上の親)を全て取得するメソッドです。
今回の例でいうとルートは1つしかないので、「ドリンク・お酒」のレコードを取得します。
ancestors_of(node)
引数で渡したレコードの一番上の親から直近の上の親までのレコードを全て取得するメソッドです。
children_of(node)メソッド
引数で渡したレコードの子レコードを全て取得するメソッドです。
descendants_of(node)メソッド
引数で渡したレコード以降の全ての階層のレコードを取得するメソッドです。
上の例だと途中で「...」と省略されていますが、lengthメソッドを使うと全ての子レコードが取得できているのが確認できます。
indirects_of(node)メソッド
引数で渡したレコードの孫レコード以下のレコードを全て取得するメソッドです。
上の例だと途中で「...」と省略されていますが、lengthメソッドを使うと全ての子レコードが取得できているのが確認できます。
subtree_of(node)メソッド
引数で渡したレコードと子レコード以下全てのレコードを取得するメソッドです。
上の例だと途中で「...」と省略されていますが、lengthメソッドを使うと全ての子レコードが取得できているのが確認できます。
siblings_of(node)メソッド
引数で渡したレコードと同じ階層のレコードを全て取得するメソッドです。
このようにancestryは親子関係のレコードの取得が簡単にできるメソッドがたくさん用意されているので大変便利ですね!
この記事のまとめ
- ancestryは多階層のテーブルを簡単に扱うことができるgemです
- 親子関係になるようなフォームを作る時に大変便利です
- 便利なメソッドも用意されているので、確認しておきましょう
