更新日:
【Rails】 ancestryを使って多階層のデータを扱おう
ancestryは簡単に多階層のデータを扱うことができるgemです。便利なメソッドも豊富に用意されています。
データを登録するフォームを作る時、多階層(親子関係)で選択したい時はありませんか?
例えば、趣味を選択するとき、大カテゴリーで「映画鑑賞」や「スポーツ観戦」、「読書」などをまず選択します。
その中で中カテゴリーで「映画鑑賞」であれば「アクション」とか「コメディ」とか「サスペンス」を選択する場合、それぞれのカテゴリーは親と子の関係になりますね。
そんな時ひとつのテーブルで階層を分けてデータを管理するときに便利なのがancestry
というgemです。
データの親子関係を理解しよう
そもそもデータの多階層とは何なのでしょうか?
簡単に言うと親子関係です。
例えば楽天市場の「ジャンル」をみてみましょう。
ここでは一番左のジャンル一覧が「親」、吹き出しで表示されている左の部分が「子」、右が「孫」という関係を表します。
このように一番上が「親」、その下に「子」、その下に「孫」という3階層になります。
このような構造を多階層構造と言います。
gemをインストールしよう
それではancestryを使う準備をしていきましょう。
Gemfileに下記のコードを追記しましょう。
1
gem 'ancestry'
その後、bundle installコマンドを実行します。
ancestryの使い方
この章ではancestryの具体的な使い方を解説していきます。
多階層のテーブルを作成しよう
今回は最初に紹介した楽天の市場のジャンルを例にして説明をしていきます。
まずはジャンルを管理するテーブルを作成します。
ターミナルで下記のrails g modelコマンドを実行し、genreモデルを作成します。
1
$ rails g model Genre
このコマンドによりモデルを定義したgenre.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カラムの追記を必ずしましょう。
その後、ターミナルで下記のコマンドを実行し、genresテーブルを作成します。
1
$ rails db:migrate
モデルを編集しよう
次にモデルを編集します。
ancestryを有効にするためにはモデルにhas_ancestry
を記述する必要があります。
appフォルダ内のmodelsフォルダにあるgenre.rb
を下記のように編集してください。
1
2
3
class Genre < ApplicationRecord
has_ancestry #このコードを追記
end
次に作成したテーブルにジャンルの名前をセットします。
この時いちいちジャンルの投稿フォームを作ってcreateアクションで保存したり、コンソールからひとつずつ入力していくというのはとても手間がかかります。
テーブルにデータを一気に作成したい場合はseedファイルを作成すると簡単にデータをセットすることができます。
seedファイルを作成してデータをセットしよう
まずはseedファイルを作成します。
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で用意されているメソッドです。
その後、ターミナルで下記のコマンドを実行するとgenresテーブルにデータがセットされます。
1
$ rails db:seed
sequel proなどでデータがセットされてるか確認してみましょう。
このようにデータがセットされていれば大丈夫です。
ancestryカラムの見方
一番上の親(ルート)に当たるレコードはancestryカラムがnullになるのがポイントです。
その子に当たるレコードはancestryカラムが親レコードのidと同じ1
となります。
ルートレコードは複数つくることもできます。
もしancestryカラムがnullのルートレコードのidが6であれば、id6の子レコードとするときはancestryカラムには6
が入ります。
そして孫カラムは1/2
のように/
で区分けされるのがポイントです。
1/2
の1
の部分はこのレコードの一番上の親のidを指します。
2
の部分は直近の親レコードのidを指します。
つまりancestryカラムに1/2
という値が入っているレコードは一番上の親のidが1
、その下の親のidは2
ということになります。
1/2/18
となっていれば一番上の親のidは1
、下の子レコードのidが2
、その下の孫レコードのidが18
となります。
このようにancestryではidの数値を「 / 」で区切って親子関係を表現しています。
もう一度先程作成したデータの中身を確認してみましょう。
例えばidが7
の「コーヒー」という名前のジャンルがあります。
このレコードの親子関係はどうなっているか確認してみましょう。
このancestryカラムの値は1/2
となっています。
つまり一番上の親のidが1
なので、「ドリンク・日本酒」のジャンルであることがわかります。
さらに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です
- 親子関係になるようなフォームを作る時に大変便利です
- 便利なメソッドも用意されているので、確認しておきましょう