Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

いまさらツリー構造

979 views

Published on

Railsでツリー構造を再帰処理を用いて隣接リストモデルで実装した場合の紹介になります。

Published in: Engineering
  • Be the first to comment

いまさらツリー構造

  1. 1. いまさらツリー構造 2018.06.28 Meguro.rb #16 Yuya Taki
  2. 2. self.inspect [1] https://github.com/muramurasan/okuribito [2] http://poject.herokuapp.com/ ➢ gemのロゴを書いたり… ➢ たまにQiitaの記事を書いたり… ➢ 新卒で某SIerに就職 ➢ 渋谷の企業にてRuby on Railsを学ぶ ➢ 今はお客さんに謝りに行くお仕事 Name : Yuya Taki GitHub : yuyasat Qiita : yuyasat [1] 若輩者ですので、何卒優しくご教授いただければと 思います。 (commitはしていない) ➢ ○よ○よ風ゲームをReact.jsで実装し たり [2]
  3. 3. 最近の知人Nとの会話 最近YouTubeばかりみてるんだけど。 うちの娘もヒカキンヒカキン言ってる。 だけど動画多くてわかりにくいし、わかりやすくカテゴリ分 けしたい。 アーティストのYouTubeページ行くといろんなのがあっ て、ミュージックPVだけ集めたい。YouTubeの音楽チャ ンネルあるけどちゃうねん。 おk。 知人N 知人N 知人N いい感じにカテゴリ分けできるサイトつくって。 サイト名は決めたから。 知人N
  4. 4. 作ってみた https://super-youtuber.com/
  5. 5. カテゴリの実装 https://super-youtuber.com/
  6. 6. はじめてのカテゴリ実装 節点(node) 葉(leaf) 枝(edge) 根(root) ➢ こんな感じのツリー構造やろ ➢ カテゴリなんていろんなところで実装されてるし余裕っしょ ➢ rubyにはancestryというgemがあるらしい ➢ 親idを持たせとけばいいっしょ id name parent_id 1 スポーツ NULL 2 野球 1 3 サッカー 1 ➢ ancestryはrootまでの経路を全部持たせるらしい ➢ 冗長じゃない?更新面倒じゃない? ➢ やっぱりこれでいけない?→ id name path 1 スポーツ NULL 2 野球 /1 3 高校野球 /1/2 ➢ とりあえずgemとか調べてみるか
  7. 7. RDBにおける木構造の実装方法 ➢ 隣接リストモデル ○ 今回実装したのがこれ。 ○ SQLアンチパターン第二章ナイーブツリー。 ➢ 経路列挙モデル ○ ancestryでの実装。 ➢ 入れ子集合(整数)モデル ➢ 入れ子区間(実数)モデル ○ ミック「データ型の制度という物理制約さえなければRDBで階層構造を扱う方法論として は、検索/更新のパフォーマンスとモデルの簡潔さの双方において最も優れている」[1] ○ でも複雑さは一番大きい。 ➢ 閉包テーブルモデル ○ 経路情報をもつテーブルを作る。 節点(node) 葉(leaf) 枝(edge) 根(root) [1] 達人に学ぶDB設計徹底指南書p291
  8. 8. 各実装の特徴 設計 テーブル数 子孫アクセス ツリーへのク エリ実行 挿入 削除 参照整合性維 持 隣接リスト 1 簡単 難しい ※再帰クエリ を使えば簡単 簡単 簡単 ※場合による 可能 経路列挙 1 簡単 簡単 簡単 簡単 不可 入れ子 1 難しい 難しい 難しい 難しい 不可 閉包テーブル 2 簡単 簡単 簡単 簡単 可能 [1] SQLアンチパターンp30 [2] SQLアンチパターンp18 [1] アンチパターンといいつつ、再帰処理ができれば用いても良い [2] 経路列挙モデルが跋扈してたので原点に戻って隣接リストモデルで実装してみた。
  9. 9. 子ノードをもつノードを安易に削除できない before_destroy do return unless children.present? errors.add(:base, '紐づいている子カテゴリを削除してください ') throw :abort end 節点(node) 葉(leaf) 枝(edge) 根(root) ➢ validationしてしまう
  10. 10. 子孫(サブツリー)の取得 ➢ rubyで再帰処理で解決! def descendants(category = self, array = [], include_self: true, only_id: true) array << (only_id ? self.id : self) if include_self && id == category.id return array + [only_id ? category.id : category] if category.children.blank? category.children.eager_load(:children).each do |cat| array << (only_id ? cat.id : cat) descendants(cat, array) end array end ➢ SQLでもかける(後述)
  11. 11. 先祖を取得するメソッドと子孫を取得するメソッド class Category < ApplicationRecord belongs_to :parent, class_name: 'Category', foreign_key: :parent_id has_many :children, class_name: 'Category', foreign_key: :parent_id def ancestors(category = self, result = [], include_self: true, only_id: true) return result + [only_id ? category.id : category] if category.root? ancestors(category.parent, result, only_id: only_id) + (!include_self && id == category.id ? [] : [only_id ? category.id : category]) end def descendants(category = self, array = [], include_self: true, only_id: true) array << (only_id ? self.id : self) if include_self && id == category.id return array + [only_id ? category.id : category] if category.children.blank? category.children.eager_load(:children).each do |cat| array << (only_id ? cat.id : cat) descendants(cat, array) end array end end 先祖を取得 子孫を取得
  12. 12. ancestryのインスタンスメソッドとの対応 ancestry 隣接リストモデル parent parent parent_id parent_id root ancestors(only_id: false).first root_id ancestors.first root? parent_id == 0 ancestors ancestors(include_self: false, only_id: false) ancestors? ancestors.length > 1 ancestor_ids ancestors(include_self: false) path ancestors(only_id: false) path_ids ancestors children children child_ids children.pluck(:id) has_parent? parent_id != 0 or self.class.exists?(id: parent_id) has_children? children.exists? childless? !children.exists? https://github.com/stefankroes/ancestry ancestry 隣接リストモデル siblings parent&.children || self.class.ro sibling_ids siblings.pluck(:id) has_siblings? siblings.exists? only_child? siblings.count == 1 descendants descendants(include_self: false, only_id: false) descendant_ids descendants(include_self: false) subtree descendants(only_id: false) subtree_ids descendants depth ancestors.count - 1
  13. 13. With `WITH RECURSIVE` def ancestors(include_self: true) ids = Category.find_by_sql(<<-SQL).map(&:id) - (include_self ? [] : [id]) WITH RECURSIVE ancestors(id, parent_id) as ( SELECT categories.id, categories.parent_id FROM categories WHERE categories.id = #{id} UNION ALL SELECT categories.id, categories.parent_id FROM ancestors, categories WHERE ancestors.parent_id = categories.id ) SELECT id FROM ancestors; SQL self.class.where(id: ids).order(ids.map { |id| "categories.id = #{id} desc" }) end
  14. 14. With `WITH RECURSIVE` def descendants(include_self: true) ids = Category.find_by_sql(<<-SQL).map(&:id) - (include_self ? [] : [id]) WITH RECURSIVE children(id, parent_id) as ( SELECT categories.id, categories.parent_id FROM categories WHERE categories.id = #{id} UNION ALL SELECT categories.id, categories.parent_id FROM categories, children WHERE children.id = categories.parent_id ) SELECT id FROM children; SQL self.class.where(id: ids).order(ids.map { |id| "categories.id = #{id} desc" }) end
  15. 15. まとめ ➢ 隣接リストモデルでも、再帰処理を用いることで先祖も子孫も簡単に取 得できる。 ➢ 再帰処理のメソッド2つ(ancestrorsとdescendants)を押さえておけば 良い。 ツリー構造の実装は要件に応じて計画的に。
  16. 16. 資料 SQLアンチパターン 達人に学ぶDB設計 徹底指南書 初級者で終わりたくないあなたへ RDBでツリー構造 [PostgreSQL 8.4+] WITH RECURSIVEの動作を理解する 木構造の親または子を再帰的に取得する SQLアンチパターン - ナイーブツリー Railsでツリー構造をもったカテゴリを隣接リストモデルで実装する
  17. 17. ご静聴ありがとうございました。
  18. 18. 隣接リストモデル カテゴリ 親 科目 NULL 数学 科目 物理 科目 力学 物理 波動 物理 熱力学 物理 物理 熱力学 科目 数学 力学 波動
  19. 19. 経路列挙モデル 物理 熱力学 科目 数学 力学 波動 カテゴリ 経路 科目 NULL 数学 /科目 物理 /科目 力学 /科目/物理 波動 /科目/物理 熱力学 /科目/物理
  20. 20. 入れ子集合モデル 科目 物理 力学 波動 熱力学 数学 1 2 3 4 5 6 7 8 9 10 11 12 カテゴリ 左端 右端 科目 1 12 数学 2 3 物理 4 11 力学 5 6 波動 7 8 熱力学 9 10

×