Skip to content

orcasnet/activerecord-recursive_tree_scopes

Repository files navigation

ActiveRecord Recursive Tree Scopes

Build Status Code Climate

Using ActiveRecord scopes, recursively traverse trees using a single SQL query.

Let's say you've got an ActiveRecord model Employee with attributes id, name, and manager_id. Using stock belongs_to and has_many relations it's easy to query for an Employee's manager and directly managed Employee's.

class Employee < ActiveRecord::Base
  belongs_to :manager,          class_name: 'Employee'
  has_many   :directly_managed, class_name: 'Employee', foreign_key: :manager_id
...

ActiveRecord Recursive Tree Scopes provides two scopes. These scopes, using a single SQL query, match all ancestors or descendants for a record in a tree.

...
  has_ancestors   :managers, key: :manager_id
  has_descendants :managed,  key: :manager_id
end

A Single Query

Yep, a single query. Thanks to PostgreSQL's WITH RECURSIVE it's possible to recursively match records in a single query.

Using the model above as an example, let's say you've got an Employee with an id of 42. Here's the SQL that would be generated for employee.managed

SELECT "employees".* 
FROM "employees" 
WHERE (
  employees.id IN (
    WITH RECURSIVE descendants_search(id, path) AS (
      SELECT id, ARRAY[id]
      FROM employees
      WHERE id = 42
      UNION ALL
        SELECT employees.id, path || employees.id
        FROM descendants_search
        JOIN employees
        ON employees.manager_id = descendants_search.id
        WHERE NOT employees.id = ANY(path)
    )
    SELECT id
    FROM descendants_search
    WHERE id != 42
    ORDER BY path
  )
)
ORDER BY employees.id

Advanced Usage

Multiple key trees are supported. For example, a Person model may have mother_id and father_id keys. That's no problem, just tell the scope about both keys. person.progenitors will return all ancestors, mothers and fathers.

class Person < ActiveRecord::Base
  belongs_to :mother, class_name: 'Person'
  belongs_to :father, class_name: 'Person'

  has_ancestors   :progenitors, key: [ :mother_id, :father_id ]
  has_descendants :progeny,     key: [ :mother_id, :father_id ]
end

Friendly

Go ahead, chain away:

employee.managers.where(name: 'Bob').exists?
SELECT "employees".* 
FROM "employees" 
WHERE 
  "employees"."name" = 'Bob' AND 
  (
    employees.id IN (
      WITH RECURSIVE descendants_search(id, path) AS (
        SELECT id, ARRAY[id]
        FROM employees
        WHERE id = 42
        UNION ALL
          SELECT employees.id, path || employees.id
          FROM descendants_search
          JOIN employees
          ON employees.manager_id = descendants_search.id
          WHERE NOT employees.id = ANY(path)
      )
      SELECT id
      FROM descendants_search
      WHERE id != 42
      ORDER BY path
    )
  )
ORDER BY employees.id

Requirements

  • ActiveRecord >= 3.1.0
  • PostgreSQL >= 8.4

Installation

Add gem 'activerecord-recursive_tree_scopes' to your Gemfile.

Alternatives

The Edge gem is similar but makes better use of Arel and relations.

Thanks

Thanks to Joshua Davey, his blog post inspired this gem.

Copyright

Copyright (c) 2013 John Wulff. See LICENSE.txt for further details.

About

Using an ActiveRecord relation, recursively traverse trees using a single SQL query.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages