Last active
January 16, 2018 18:45
-
-
Save sdhull/a7d35d04ca3e579d08d68e0fae8d50d8 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Node < ApplicationRecord | |
# class that implements materialized path hierarchy | |
# a la https://medium.com/notes-from-a-messy-desk/representing-trees-in-postgresql-cbcdae419022 | |
# Normal associations don't work | |
# | |
# has_many :children, ->(node) { where(path: node.path + [node.id]) }, class_name: "Node", foreign_key: nil | |
# primary_key: nil, | |
# inverse_of: :parent | |
# belongs_to :parent, ->(node) { find_by_sql(id: node.path.last) }, | |
# class_name: "Node", | |
# foreign_key: nil, | |
# primary_key: nil, | |
# inverse_of: :children, | |
# optional: true | |
def ancestors | |
if path.present? | |
@ancestors ||= self.class.where(id: path) | |
else | |
self.class.none | |
end | |
end | |
def children | |
@children ||= self.class.where(path: path + [id]) | |
end | |
def siblings | |
@siblings ||= self.class.where(path: path) | |
end | |
def descendants | |
@descendants ||= self.class.where(":id = ANY(path)", id: id) | |
end | |
def parent=(node) | |
self.path = node.path + [node.id] | |
@root = @parent.root | |
@parent = node | |
end | |
def parent | |
@parent ||= self.class.find_by_id(self.path.last) if self.path.last | |
end | |
def root | |
@root ||= if root_id = path.first | |
self.class.find(root_id) | |
else | |
self | |
end | |
end | |
scope :roots { where(path: []) } | |
# Must be last in scope method chain | |
scope :load_descendants!, -> { | |
scope_copy = all.load | |
ids = scope_copy.map(&:id) | |
inclusive_descendants = unscoped.where("(path && ARRAY[:ids]::int[]) OR (id IN (:ids))", ids: ids) | |
id_map = {} | |
child_map = Hash.new { Array.new } | |
inclusive_descendants.each do |descendant| | |
id_map[descendant.id] = descendant | |
child_map[descendant.path.last] += [descendant] | |
end | |
scope_copy.each do |node| | |
id_map[node.id] = node | |
end | |
child_map.each do |id, children| | |
next if id.nil? # roots | |
parent = id_map[id] | |
parent.children.send(:load_records, children) | |
children.each { |c| c.parent = parent } | |
end | |
scope_copy | |
} | |
# if you know for sure that a parent will always | |
# be earlier in the descendants list than its own children | |
def load_descendants! | |
id_map = {id: self} | |
descendants.each do |descendant| | |
id_map[descendant.id] = descendant | |
if parent = id_map[descendant.path.last] | |
if parent.children.instance_variable_get("@records").frozen? | |
parent.children.instance_variable_set("@records", []) | |
parent.children.instance_variable_set("@loaded", true) # no way of knowing if this is the last child for this parent | |
end | |
parent.children.records.push(descendant) | |
end | |
end | |
self | |
end | |
# if children may appear in the #descendants list prior to their | |
# own parents | |
def load_descendants! | |
id_map = {id: self} | |
child_map = Hash.new { Array.new } | |
descendants.each do |descendant| | |
id_map[descendant.id] = descendant | |
child_map[descendant.path.last] += [descendant] | |
end | |
child_map.each do |id, children| | |
parent = id_map[id] | |
parent.children.send(:load_records, children) | |
end | |
self | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment