こんにちは。
GMOアドマーケティングのR.Sです。
今回はActiveRecordのモデルをツリー構造で整理できるようにする「ancestry」というgemを紹介します。
親子孫ひ孫のような多階層構造が簡単に実装できるので、カテゴリ分けなどに利用できます。
動作環境
-
Ruby 2.4.4
-
Rails 5.2.4
-
ancestry 3.2.1
gemインストール
1 |
gem 'ancestry' |
$ bundle install
テーブル作成
まずモデルを作成して
$ bundle exec rails g model Category
ancestryカラムを含むようにマイグレーションファイルを編集、
1 2 3 4 5 6 7 8 9 |
class CreateCategories < ActiveRecord::Migration[5.2] def change create_table :categories do |t| t.string :name, null: false, comment: 'カテゴリ名' t.string :ancestry, comment: 'ancestry使用' t.timestamps end end end |
マイグレーション実行。
bundle exec rake db:migrate
モデルを下記のように編集します。
1 2 3 4 |
class Category < ApplicationRecord # この1行を追加 has_ancestry end |
has_ancestryを追記することでancestryが有効になりました。
データの投入
今回はこちらの記事を参考にseedファイルを作成してデータを投入しました。
ancestryカラムがnullのレコードはツリーでいうroot(一番上の親)にあたります。
子は1、孫は1/2というように/で区切られて階層が表現されるので、ID5のファンデーションの親はID2の化粧品、ID2の化粧品の親はID1の美容という経路で追うことができます。
下図のように、ひ孫を設定することも可能です。
メソッドの活用
ancestryを使うメリットとして、メソッドが豊富に用意されていることが挙げられます。
公式ドキュメントにツリー構造の図と共に記載してあります。丸で囲まれたノードから黄色のノードを得ることができます。
コンソールを起動してメソッドを使用してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
biyou = Category.find(1) => #<Category id: 1, name: "美容", ancestry: "", created_at: nil, updated_at: nil> biyou.parent => nil # rootの親を取得しようとするとnilを返す cosme = Category.find(2) => #<Category id: 2, name: "化粧品", ancestry: "1", created_at: nil, updated_at: nil> cosme.parent => #<Category id: 1, name: "美容", ancestry: "", created_at: nil, updated_at: nil> cosme.parent_id => 1 cosme.root_id => 1 # rootのIDも取得できる |
1 2 3 4 5 6 7 8 9 |
biyou.children => #<ActiveRecord::Relation [#<Category id: 2, name: "化粧品", ancestry: "1", created_at: nil, updated_at: nil>, #<Category id: 3, name: "ヘアケア", ancestry: "1", created_at: nil, updated_at: nil>, #<Category id: 4, name: "ボディケア", ancestry: "1", created_at: nil, updated_at: nil>]> biyou.child_ids => [2, 3, 4] cosme.children => #<ActiveRecord::Relation [#<Category id: 5, name: "ファンデーション", ancestry: "1/2", created_at: nil, updated_at: nil>, #<Category id: 6, name: "アイシャドウ", ancestry: "1/2", created_at: nil, updated_at: nil>, #<Category id: 7, name: "チーク", ancestry: "1/2", created_at: nil, updated_at: nil>]> cosme.child_ids => [5, 6, 7] |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
親に向かって経路を辿る powder = Category.find(13) => #<Category id: 13, name: "パウダーファンデーション", ancestry: "1/2/5", created_at: nil, updated_at: nil> powder.ancestors => #<ActiveRecord::Relation [#<Category id: 1, name: "美容", ancestry: "", created_at: nil, updated_at: nil>, #<Category id: 2, name: "化粧品", ancestry: "1", created_at: nil, updated_at: nil>, #<Category id: 5, name: "ファンデーション", ancestry: "1/2", created_at: nil, updated_at: nil>]> powder.ancestor_ids => [1, 2, 5] 子に向かって経路を辿る foundation = Category.find(5) => #<Category id: 5, name: "ファンデーション", ancestry: "1/2", created_at: nil, updated_at: nil> foundation.subtree => #<ActiveRecord::Relation [#<Category id: 5, name: "ファンデーション", ancestry: "1/2", created_at: nil, updated_at: nil>, #<Category id: 13, name: "パウダーファンデーション", ancestry: "1/2/5", created_at: nil, updated_at: nil>, #<Category id: 14, name: "クッションファンデーション", ancestry: "1/2/5", created_at: nil, updated_at: nil>]> foundation.subtree_ids => [5, 13, 14] |
まとめ
ancestryを使うことで、ものすごく簡単にカテゴリ分けをすることができました。
経路がancestryというひとつのカラムに集約しているため、頻繁に経路が変わるものには使いづらいというデメリットもあります。
とはいえ、親から見た子にあたるレコードを全て取得できたり、孫やひ孫からも親をすぐ辿れるメソッドが用意されているメリットは大きいと思いました。