這是用戶在 2024-6-5 16:20 為 https://pikawaka.com/rails/ancestry 保存的雙語快照頁面,由 沉浸式翻譯 提供雙語支持。了解如何保存?

すでにメンバーの場合は  已經是成員的話,請登入

無料会員登録

GitHubアカウントで登録  使用 GitHub 帳戶註冊Pikawakaが許可なくTwitterやFacebookに投稿することはありません。
Pikawaka 不會未經許可在 Twitter 或 Facebook 上發布。

登録がまだの方はこちらから  請從這裡進行註冊

Pikawakaにログイン  Pikawaka 登入

GitHubアカウントでログイン  使用 GitHub 帳戶登入Pikawakaが許可なくTwitterやFacebookに投稿することはありません。
Pikawaka 不會未經許可在 Twitter 或 Facebook 上發布。

Rails

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

ぴっかちゃん
ぴっかちゃん 皮卡酱

ancestryは簡単に多階層のデータを扱うことができるgemです。便利なメソッドも豊富に用意されています。
ancestry 是一個可以輕鬆處理多層次數據的 gem。它提供了豐富的方便方法。

データを登録するフォームを作る時、多階層(親子関係)で選択したい時はありませんか?
您在製作登錄數據的表單時,是否有時想要在多層次(父子關係)中進行選擇?

例えば、趣味を選択するとき、大カテゴリーで「映画鑑賞」や「スポーツ観戦」、「読書」などをまず選択します。
例如,當選擇興趣時,首先在大類別中選擇「觀賞電影」、「觀看體育比賽」、「閱讀」等。

その中で中カテゴリーで「映画鑑賞」であれば「アクション」とか「コメディ」とか「サスペンス」を選択する場合、それぞれのカテゴリーは親と子の関係になりますね。
在這些中,如果在中類別中選擇「電影觀賞」,則「動作」、「喜劇」或「懸疑」等,每個類別都會成為父子關係。

そんな時ひとつのテーブルで階層を分けてデータを管理するときに便利なのがancestryというgemです。
在這種情況下,一個很方便的 gem 是 ancestry ,可以在一個表中分層管理數據。

データの親子関係を理解しよう  讓我們了解數據之間的親子關係

そもそもデータの多階層とは何なのでしょうか? 從根本上來說,多層次數據是什麼?
簡単に言うと親子関係です。 簡單來說就是親子關係。

例えば楽天市場の「ジャンル」をみてみましょう。 讓我們來看看樂天市場的「類別」。

ジャンル一覧

ここでは一番左のジャンル一覧が「親」、吹き出しで表示されている左の部分が「子」、右が「孫」という関係を表します。
這裡最左邊的類別列表是「父」,在氣泡中顯示的左邊部分是「子」,右邊是「孫」,表示它們之間的關係。

親子関係図

このように一番上が「親」、その下に「子」、その下に「孫」という3階層になります。
這樣最上層是「親」,其下是「子」,再下是「孫」,形成了三層結構。

このような構造を多階層構造と言います。 這種結構被稱為多層結構。

gemをインストールしよう  安裝 gem

それではancestryを使う準備をしていきましょう。
讓我們開始準備使用 ancestry。

Gemfileに下記のコードを追記しましょう。 讓我們在 Gemfile 中添加以下代碼。

Gemfile | gemの追加
1
gem 'ancestry'

その後、bundle installコマンドを実行します。
然後,執行 bundle install 命令。

ancestryの使い方  祖先的使用方式

この章ではancestryの具体的な使い方を解説していきます。
在這一章中,我們將解釋 ancestry 的具體用法。

多階層のテーブルを作成しよう  建立多層次的表格

今回は最初に紹介した楽天の市場のジャンルを例にして説明をしていきます。
這次將以最初介紹的樂天市場的類別作為例子進行解釋。

まずはジャンルを管理するテーブルを作成します。 首先創建一個管理類型的表。
ターミナルで下記のrails g modelコマンドを実行し、genreモデルを作成します。
在終端機上運行以下的 rails g model 命令,創建 genre 模型。

ターミナル | genreモデルの作成 終端 | 創建 genre 模型
1
$ rails g model Genre

このコマンドによりモデルを定義したgenre.rbとgenresテーブルを作成するマイグレーションファイルなどが作成されます。
通過這個命令,將定義模型的 genre.rb 和創建 genres 表的遷移文件等將被創建。

作成されたマイグレーションファイルを下記のように編集してください。
請編輯已創建的遷移文件如下所示。

db/migrate/バージョン名_create_genres.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 表。

ターミナル | genreテーブルの作成 終端 | 創建 genre 表
1
$ rails db:migrate

モデルを編集しよう  編輯模型

次にモデルを編集します。 接下來編輯模型。
ancestryを有効にするためにはモデルにhas_ancestryを記述する必要があります。
為了啟用 ancestry,您需要在模型中記錄 has_ancestry

appフォルダ内のmodelsフォルダにあるgenre.rbを下記のように編集してください。
請將位於 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 進行以下編輯。

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 表中。

ターミナル | seedsファイルの実行 終端 | 執行 seeds 檔案
1
$ rails db:seed

sequel proなどでデータがセットされてるか確認してみましょう。
讓我們在 sequel pro 等工具中檢查數據是否已設置。

データの確認

このようにデータがセットされていれば大丈夫です。
如果數據設置正確,就沒問題。

ancestryカラムの見方  祖先列的查看方式

一番上の親(ルート)に当たるレコードはancestryカラムがnullになるのがポイントです。
一番上的父母(根)對應的記錄是祖先列為空的關鍵。

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/21の部分はこのレコードの一番上の親のidを指します。
1/21 部分指的是這條記錄的最上層父級 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
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です
  • 親子関係になるようなフォームを作る時に大変便利です
  • 便利なメソッドも用意されているので、確認しておきましょう