Question

I have a hierarchical structure of pages when one page can be a parent of another. Pages are stored in a DB table with the following structure:

  • page.id
  • page.name
  • page.parent_id

Now I need to output them is a hierarchical list like:

  • Page A
    • Page B
    • Page C
      • Page D
      • etc
  • Page E

Also there is no limit to a number of levels.

What would be the best approach to achieve that in RoR? Thanks in advance.

Was it helpful?

Solution 2

Hi i would use some gem to do this

i.e.

https://github.com/stefankroes/ancestry

OTHER TIPS

Here are the best practices to handle - https://blog.francium.tech/best-practices-for-handling-hierarchical-data-structure-in-ruby-on-rails-b5830c5ea64d

A Simple table

Table Emp
id: Integer
name: String
parent_id: Integer

Associations

app/models/emp.rb
class Emp < ApplicationRecord
  has_many :subs, class_name: 'Emp', foreign_key: :parent_id
  belongs_to :superior, class_name: 'Emp', foreign_key: :parent_id
end

Scope Definition

class Emp < ApplicationRecord
  ----
  ----
  scope :roots, -> { where(parent_id: nil) }
end

Fetching data

def tree_data
  output = []
  Emp.roots.each do |emp|
    output << data(emp)
  end
  output.to_json
end
def data(employee)
  subordinates = []
  unless employee.subs.blank?
    employee.subs.each do |emp|
      subordinates << data(emp)
    end
  end
  {name: employee.name, subordinates: subordinates}
end

Eager Loading

def tree_data
  output = []
  Emp.roots.includes(subs: {subs: {subs: subs}}}.each do |emp|
    output << data(emp)
  end
  output.to_json
end

Hypothetical code, not verified.

class Page < ActiveRecord::Base
  has_many :children, class_name: 'Page', foreign_key: 'parent_id'
  belongs_to :parent, class_name: 'Page'
end

# Helper
def tree_list(collection)
  content_tag :ul do
    collection.each do |item|
      if item.children.blank?
        content_tag :li do
          item.name
        end
      else
        tree_list(item)
      end
    end
  end 
end

# View
tree_list(Page.all)

The basic idea is the tree_list method will make a recursive list until there is no children for a certain page.

As another user suggested, you could use ancestry gem but it will require changes to your database. If you can't do that you should create a directed acyclic graph and set depth for each node.

class Page < ActiveRecord::Base
  attr_accessor :depth

  def children
    @children ||= []
  end
end

# Loading required pages in single query, add predicate if needed
pages = Page.includes(:parent).all

# Setting up a hash table to find pages in constant time
page_index = pages.reduce({}){|acc, page| acc[page.id] = page}

# Setting up children
pages.each do |page|
  if page.parent_id
    page_index[page.parent_id].children << page
  end
end

# Basic depth-first search routine
def dfs(page, visited, depth)
  page.depth = depth
  visited[page.id] = true
  page.children.select{|p| visited[p] == nil}.each{|p| dfs(p, visited, depth + 1)}
end

# Performing depth-first search for each connected component
visited = {}
pages.each do |page|
  if visited[page.id] == nil
    dfs(page, visited, 0)
  end
end

So, now each page has a depth attribute and you can print the list however you like.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top