すでにメンバーの場合は

無料会員登録

GitHubアカウントで登録 Pikawakaが許可なくTwitterやFacebookに投稿することはありません。

登録がまだの方はこちらから

Pikawakaにログイン

GitHubアカウントでログイン Pikawakaが許可なくTwitterやFacebookに投稿することはありません。

Rails

【Rails】 ancestryを使って多階層のデータを扱おう

ぴっかちゃん
ぴっかちゃん

ancestryは簡単に多階層のデータを扱うことができるgemです。便利なメソッドも豊富に用意されています。

データを登録するフォームを作る時、多階層(親子関係)で選択したい時はありませんか?

例えば、趣味を選択するとき、大カテゴリーで「映画鑑賞」や「スポーツ観戦」、「読書」などをまず選択します。

その中で中カテゴリーで「映画鑑賞」であれば「アクション」とか「コメディ」とか「サスペンス」を選択する場合、それぞれのカテゴリーは親と子の関係になりますね。

そんな時ひとつのテーブルで階層を分けてデータを管理するときに便利なのがancestryというgemです。

データの親子関係を理解しよう

そもそもデータの多階層とは何なのでしょうか?
簡単に言うと親子関係です。

例えば楽天市場の「ジャンル」をみてみましょう。

ジャンル一覧

ここでは一番左のジャンル一覧が「親」、吹き出しで表示されている左の部分が「子」、右が「孫」という関係を表します。

親子関係図

このように一番上が「親」、その下に「子」、その下に「孫」という3階層になります。
このような構造を多階層構造と言います。

gemをインストールしよう

それではancestryを使う準備をしていきましょう。
Gemfileに下記のコードを追記しましょう。

Gemfile | gemの追加
1
gem 'ancestry'

その後、bundle installコマンドを実行します。

ancestryの使い方

この章ではancestryの具体的な使い方を解説していきます。

多階層のテーブルを作成しよう

今回は最初に紹介した楽天の市場のジャンルを例にして説明をしていきます。
まずはジャンルを管理するテーブルを作成します。
ターミナルで下記のrails g modelコマンドを実行し、genreモデルを作成します。

ターミナル | genreモデルの作成
1
$ rails g model Genre

このコマンドによりモデルを定義した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カラムの追記を必ずしましょう。

その後、ターミナルで下記のコマンドを実行し、genresテーブルを作成します。

ターミナル | genreテーブルの作成
1
$ rails db:migrate

モデルを編集しよう

次にモデルを編集します。
ancestryを有効にするためにはモデルにhas_ancestryを記述する必要があります。
appフォルダ内のmodelsフォルダにあるgenre.rbを下記のように編集してください。

app/models/genre.rb | コードの追記
1
2
3
class Genre < ApplicationRecord
  has_ancestry #このコードを追記
end

次に作成したテーブルにジャンルの名前をセットします。
この時いちいちジャンルの投稿フォームを作ってcreateアクションで保存したり、コンソールからひとつずつ入力していくというのはとても手間がかかります。

テーブルにデータを一気に作成したい場合は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で用意されているメソッドです。

その後、ターミナルで下記のコマンドを実行するとgenresテーブルにデータがセットされます。

ターミナル | seedsファイルの実行
1
$ rails db:seed

sequel proなどでデータがセットされてるか確認してみましょう。

データの確認

このようにデータがセットされていれば大丈夫です。

ancestryカラムの見方

一番上の親(ルート)に当たるレコードはancestryカラムがnullになるのがポイントです。

null

その子に当たるレコードはancestryカラムが親レコードのidと同じ1となります。

ルートレコードは複数つくることもできます。
もしancestryカラムがnullのルートレコードのidが6であれば、id6の子レコードとするときはancestryカラムには6が入ります。

そして孫カラムは1/2のように/で区分けされるのがポイントです。
1/21の部分はこのレコードの一番上の親の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
parent

parentの例

▪️例B
parent

parentの例

parent_idメソッド

parentメソッドで取得したレコードのidを取得します。

parent_id
自分の親が存在しない場合はnilが返ります。

parent_id
上の例だと一番上のルートレコードに使っているため、親が存在せずnilが返ります。

rootメソッド

一番上の親レコード(ルート)を取得します。
一番上の親はひとつなので原則1つのレコードを取得します。
ルートのレコードに使用すると自分自身を返します。

root

rootの例1rootの例2

root_idメソッド

rootメソッドで取得したレコードのidを取得します。
root_id

root? / is_root?メソッド

レコードが一番上の親レコード(ルート)であればtrueを返すメソッドです。
rootメソッドの画像のAのレコードがルートレコードなので、Aのレコードに対して使えばtrueが、それ以外のレコードだとfalseが返ります。
2つのメソッドは同じ返り値を返すので、どちらを使ってもOKです。

root?

ancestorsメソッド

自分の一番上の親(ルート)から直近の上の親までのレコードを取得するメソッドです。

ancestors
ancestorsの例1ancestorsの例2

ancestor_idsメソッド

自分の親レコード全てのidを取得するメソッドです。
ancestorsメソッドの画像の例だと黄色のレコードのidを全て取得します。

ancestor_ids

pathメソッド

自分の一番上の親(ルート)から自分で終わるレコード全てを取得します。
ancestorsメソッドとは違い自分のレコードも取得します。
path
pathメソッドpathメソッド

path_idsメソッド

上のpathメソッドで取得したレコードのidを全て取得します。
path_ids

childrenメソッド

自分の子のレコードを全て取得するメソッドです。
children
childrenメソッドchildrenメソッド

child_idsメソッド

自分の子のレコードのidを全て取得するメソッドです。
childrenメソッドで取得できるレコードのidを全て取得します。
child_ids

has_parent? / ancestors?メソッド

自分の親レコードが存在すればtrueが返るメソッドです。
下の図だと「ドリンク・お酒」のレコードに使った場合は親レコードが存在しないのでfalseが返ります。
逆にその他のレコードはそれぞれ親が存在するのでtrueが返ります。
2つのメソッドは同じ返り値を返すので、どちらを使ってもOKです。

has_parent?
has_parent?
ancestors?

has_children? / children?メソッド

自分の子レコードが存在すればtrueが返るメソッドです。
2つのメソッドは同じ返り値を返すので、どちらを使ってもOKです。

下の図だと「ドリンク・お酒」から「ワイン」のレコードに使った場合は子レコードが存在するのでtrueが返ります。
逆に「水・ミネラルウォーター」から「スパークリングワイン・シャンパン」のレコードは子が存在しないのでfalseが返ります。

has_children?
has_children?
children?

is_childless? / childless?メソッド

上のメソッドと逆の結果が返ってくるメソッドです。
先程の図だと「ドリンク・お酒」から「ワイン」のレコードに使った場合は子レコードが存在するのでfalseが返ります。
逆に「水・ミネラルウォーター」から「スパークリングワイン・シャンパン」のレコードは子が存在しないのでtrueが返ります。
2つのメソッドは同じ返り値を返すので、どちらを使ってもOKです。

has_children?
is_childless?
childless?

siblingsメソッド

自分と同じ階層のレコードを全て取得するメソッドです。
自分も含めて全てを取得します。
siblings
siblingsメソッド

sibling_idsメソッド

自分と同じ階層のレコードのidを全て取得するメソッドです。
sibling_ids

has_siblings? / siblings?メソッド

自分の親が1つ以上の子レコードを持っていたらtrueが返るメソッドです。
下の図の場合、「ビール・洋酒」の親は「ドリンク・お酒」です。
「ドリンク・お酒」は1つ以上の子レコードを持っているのでtrueが返ります。
2つのメソッドは同じ返り値を返すので、どちらを使ってもOKです。

has_siblings?
has_siblings?

is_only_child? / only_child?メソッド

自分が親レコードの唯一の子レコードであればtrueが返るメソッドです。
2つのメソッドは同じ返り値を返すので、どちらを使ってもOKです。

全体図

上の図の場合、E、H、I、J、K、Lがtrueを返します。

descendantsメソッド

自分の子レコード以降の全ての階層のレコードを取得するメソッドです。
descendants
descendants
上の例だと途中で「...」と省略されていますが、lengthメソッドを使うと全ての子レコードが取得できているのが確認できます。

descendantsメソッドdescendantsメソッド

descendant_idsメソッド

自分の子レコード以降の全ての階層のレコードのidを取得するメソッドです。
descendantsメソッドで取得できるレコードのidを取得します。
descendant_ids

indirectsメソッド

自分の孫レコード以下のレコードを全て取得するメソッドです。
indirects
indirects
上の例だと途中で「...」と省略されていますが、lengthメソッドを使うと全ての孫レコードが取得できているのが確認できます。
indirectsメソッドindirectsメソッド

indirect_idsメソッド

自分の孫レコード以下のレコードのidを全て取得するメソッドです。
indirectsメソッドで取得できるレコードのidを取得します。
indirect_ids

subtreeメソッド

自分のレコードと子レコード以下全てのレコードを取得するメソッドです。
subtree
subtree
上の例だと途中で「...」と省略されていますが、lengthメソッドを使うと全てのレコードが取得できているのが確認できます。
subtreeメソッドsubtreeメソッド

subtree_idsメソッド

自分のレコードと子レコード以下全てのレコードのidを取得するメソッドです。
subtreeメソッドで取得できるレコードのidを取得します。
subtree_ids

depthメソッド

自分のレコードの上にどれくらいの階層があるのかを返すメソッドです。
depth
「ドリンク・お酒」の場合はルートなので0が返ります。
「水・ソフトドリンク」の場合は上に「ドリンク・お酒」の階層があるので1が返ります。
「ビール・発泡酒」の場合は上に「ドリンク・お酒」と「ビール・洋酒」の階層があるので2が返ります。
depth

全体図

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つしかないので、「ドリンク・お酒」のレコードを取得します。

roots

ancestors_of(node)

引数で渡したレコードの一番上の親から直近の上の親までのレコードを全て取得するメソッドです。
ancestors_of
ancestorsの例1ancestorsの例2

children_of(node)メソッド

引数で渡したレコードの子レコードを全て取得するメソッドです。
children_of
childrenメソッドchildrenメソッド

descendants_of(node)メソッド

引数で渡したレコード以降の全ての階層のレコードを取得するメソッドです。
descendants
descendants_of
上の例だと途中で「...」と省略されていますが、lengthメソッドを使うと全ての子レコードが取得できているのが確認できます。

descendantsメソッドdescendantsメソッド

indirects_of(node)メソッド

引数で渡したレコードの孫レコード以下のレコードを全て取得するメソッドです。
indirects
indirects_of
上の例だと途中で「...」と省略されていますが、lengthメソッドを使うと全ての子レコードが取得できているのが確認できます。

indirectsメソッドindirectsメソッド

subtree_of(node)メソッド

引数で渡したレコードと子レコード以下全てのレコードを取得するメソッドです。
subtree
subtree_of
上の例だと途中で「...」と省略されていますが、lengthメソッドを使うと全ての子レコードが取得できているのが確認できます。

subtreeメソッドsubtreeメソッド

siblings_of(node)メソッド

引数で渡したレコードと同じ階層のレコードを全て取得するメソッドです。
siblings_of
siblingsメソッド

このようにancestryは親子関係のレコードの取得が簡単にできるメソッドがたくさん用意されているので大変便利ですね!

この記事のまとめ

  • ancestryは多階層のテーブルを簡単に扱うことができるgemです
  • 親子関係になるようなフォームを作る時に大変便利です
  • 便利なメソッドも用意されているので、確認しておきましょう