ancestryで並び替え可能な木構造モデルを作る
目的
ancestryを使い、orderカラムの値によって並び替えを行う木構造モデルを作る。
環境
- Ruby: 2.5.7
- Rails: 6.0.2.1
- ancestry: 4.0.0
まえがき
今回使うancestryは経路列挙モデルを採用している。
そのため、通常であればジェイウォークと呼ばれる典型的なSQLアンチパターンの諸問題を抱えてしまうことになる。
しかしこのgemはその諸問題を回避しながらうまく参照、操作することを目的に作られているため、ancestryのメソッド経由で関連モデルをいじる分には特に意識しなくてもいい。
ただ1点、需要が高いと思う並び替え機能(ソート機能)については現時点(2021/06/05)では
The sort_by_ancestry class method: TreeNode.sort_by_ancestry(array_of_nodes) can be used to sort an array of nodes as if traversing in preorder. (Note that since materialised path trees do not support ordering within a rank, the order of siblings is dependant upon their original array order.)
ということで、よしなにはやってくれないらしい。
いろいろ調べても再帰処理クエリを書いて地道に木構造を作っていたり、独自のメソッドを組み合わせて実装している記事しか見当たらなかった。
しかしgemの中身を掘ってみるとブロック変数をメソッドに渡せば指定カラムの値で並び替えができるようだった。
せっかくなのでこのgemの強みを生かしてシンプルに実装していきたい。
導入
木構造モデルの作成
$ rails g scaffold Tree name:text order:integer ancestry:string:index
Gemfileにancestryを追加
gem 'ancestry'
bundle install、migration
$ bundle install
$ rails db:migrate
木構造モデルにancestry機能を付与
class Tree < ActiveRecord::Base
has_ancestry
validates :name, presence: true
validates :order, presence: true, uniqueness: true
...
end
実装
# 並び替え(順番下げ)が可能かを返す
def can_order_down?
lower_bound_sibling_order.present?
end
# 並び替え(順番上げ)が可能かを返す
def can_order_up?
upper_bound_sibling_order.present?
end
# 直前のorderの兄弟を返す
def prev_sibling
siblings.where('order < ?', order).order(order: :desc).first if can_order_down?
end
# 直後のorderの兄弟を返す
def next_sibling
siblings.where('order > ?', order).order(order: :asc).first if can_order_up?
end
# 兄弟のorderを入れ替える
def swap_order_with_sibling
sibling_tree = yield(self)
return unless sibling_tree
self_order, sibling_order = order, sibling_tree.order
transaction do
update!(order: sibling_order)
sibling_tree.update!(order: self_order)
end
end
private
def lower_bound_sibling_order
siblings.pluck(:order).reject { |o| o >= order }.max
end
def upper_bound_sibling_order
siblings.pluck(:order).reject { |o| o <= order }.min
end
resources :trees do
collection do
get :up_order
get :down_order
end
end
# order順を含めながら、木構造でTreeモデルを並べる
def index
sort = -> (a, b) { a.order <=> b.order }
@trees = Tree.sort_by_ancestry(Tree.all, &sort)
end
def up_order
Tree.find(params[:id]).swap_order_with_sibling { |tree| tree.next_sibling }
redirect_to trees_path
end
def down_order
Tree.find(params[:id]).swap_order_with_sibling { |tree| tree.prev_sibling }
redirect_to trees_path
end
table
thead
tr
td order
td name
tbody
- @trees.each.with_index do |tree, i|
tr
td
- if i > 0 && @trees[i].can_order_down?
= link_to down_order_trees_path(id: tree.id), remote: true do
|↑
- else
|↑
- if @trees[i + 1].present? && @trees[i].can_order_up?
= link_to up_order_trees_path(id: tree.id), remote: true do
|↓
- else
|↓
td
= "ー" * tree.depth + tree.name
実装画面スクリーンショット
あとがき
こういった実装はSiblingOrderable
のようなconcernとして切り出して再利用できる形にしたくなるとは思うが、この記事の趣旨からは外れるので割愛
Discussion