Closure Tree

repository·master·Indexed 24 days ago

https://github.com/closuretree/closure_tree

A high-performance Ruby gem that enables ActiveRecord models to function as nodes in a tree data structure using the closure table pattern. It supports hierarchical data modeling for tags, comments, or page graphs and is compatible with ActiveRecord 7.2+ across MySQL, PostgreSQL, and SQLite. Features include path-based node creation, eager loading, deterministic ordering, and support for Single Table Inheritance (STI).

Tokens
7.3K
Snippets
19
Records
38
Agent score
82%

What's inside closure_tree

  1. Manage concurrency and advisory locks

    master

    Methods like #rebuild and #find_or_create_by_path are not safe for concurrent execution and can cause data corruption or duplicate nodes. Closure Tree uses with_advisory_lock to ensure correctness across PostgreSQL and MySQL.

    Disabling locks: You can disable advisory locks by passing with_advisory_lock: false. Warning: If you disable this and perform multi-threaded writes without an alternative mutex, you will eventually experience data corruption.

    Customizing lock names: You can customize the advisory lock name to avoid collisions or to implement multi-tenancy. Supported types:

    • Static String: advisory_lock_name: 'custom_lock'
    • Proc (1-arity): Receives the model class. advisory_lock_name: ->(model_class) { ... }
    • Model Method: Delegates to a class method. advisory_lock_name: :custom_lock_name
    • Proc (2-arity): Receives the model class and the instance. This is recommended for scoped/multi-tenant models to ensure each tenant has its own lock.
    # Static string
    class Tag < ApplicationRecord
      has_closure_tree advisory_lock_name: 'custom_tag_lock'
    end
    
    # Dynamic via Proc (1-arity)
    class Tag < ApplicationRecord
      has_closure_tree advisory_lock_name: ->(model_class) { "#{Rails.env}_#{model_class.name.underscore}" }
    end
    
    # Delegate to model method
    class Tag < ApplicationRecord
      has_closure_tree advisory_lock_name: :custom_lock_name
    
      def self.custom_lock_name
        "tag_lock_#{current_tenant_id}"
      end
    end
    
    # Per-instance lock names (2-arity) for multi-tenancy
    class Node < ApplicationRecord
      has_closure_tree scope: :company_id,
                       advisory_lock_name: ->(klass, instance) {
                         company = instance&.company_id
                         company ? "ct_#{klass.name}_#{company}" : "ct_#{klass.name}"
                       }
    end
  2. Use `rebuild!` when using Rails test fixtures

    master

    Because Rails test fixtures do not trigger after_save hooks during insertion, the hierarchy table will not be automatically maintained. You must call .rebuild! on your model class before your tests run to reconstruct the tree structure.

      describe "Tag with fixtures" do
        fixtures :tags
        before :each do
          Tag.rebuild! # <- required if you use fixtures
        end
  3. Run tests against different databases

    master

    You can test the closure_tree implementation against different database engines by setting the DATABASE_URL environment variable when running the test suite.

    # Run with PostgreSQL
    DATABASE_URL=postgres://localhost/closure_tree_test rake test
    
    # Run with MySQL  
    DATABASE_URL=mysql2://localhost/closure_tree_test rake test
  4. Move nodes within the tree

    master

    You can reparent nodes by either using the add_child method or by updating the parent_id directly. closure_tree automatically handles the movement of the node and all its descendants to the new location in the hierarchy.

    # Using add_child
    d = Tag.find_or_create_by_path %w[a b c d]
    h = Tag.find_or_create_by_path %w[e f g h]
    e = h.root
    d.add_child(e)
    h.ancestry_path #=> ["a", "b", "c", "d", "e", "f", "g", "h"]
    
    # Using direct parent_id update
    j = Tag.find 102
    j.update parent_id: 96
  5. Install closure_tree

    master

    To use closure_tree in your ActiveRecord project, follow these steps:

    1. Add gem 'closure_tree' to your Gemfile and run bundle install.
    2. Add has_closure_tree (or the alias acts_as_tree) to your hierarchical model. Important: Place this call after attr_accessible and self.table_name = lines.
    3. Add a nullable parent_id integer column to your model's table via a migration.
    4. Generate and run the closure tree migration using rails g closure_tree:migration <model_name>.
    5. Run rake db:migrate.

    Note: closure_tree requires ActiveRecord 7.2+ and supports MySQL, PostgreSQL, and SQLite.

    # 1. Gemfile
    gem 'closure_tree'
    
    # 2. Model
    class Tag < ApplicationRecord
      has_closure_tree
    end
    
    # 3. Migration
    class AddParentIdToTag < ActiveRecord::Migration[7.2]
      def change
        add_column :tags, :parent_id, :integer
      end
    end
  6. Eager loading in closure_tree

    master

    Since closure_tree methods like children return standard ActiveRecord scopes, you can use .includes to eager load associations.

    • To eager load associations for immediate children: comment.children.includes(:author).
    • To eager load associations for the entire tree: comment.self_and_descendants.includes(:author). Note that this returns an Array of objects, so the tree structure is lost in the resulting collection.
  7. Create nodes in a tree

    master

    You can build a hierarchy using several methods:

    • Root nodes: Create a node with a nil parent.
    • Children collection: Use .children.create(...) or the << operator on the children association.
    • add_child method: Use the add_child method on a parent node.
    • Direct parent assignment: Set the parent attribute when creating a new record.

    Once created, you can access the lineage using .ancestry_path or fetch a node and all its descendants using .self_and_descendants.

    # Create a root node
    grandparent = Tag.create(name: 'Grandparent')
    
    # Create children via collection
    parent = grandparent.children.create(name: 'Parent')
    child2 = Tag.new(name: 'Second Child')
    parent.children << child2
    
    # Create children via add_child
    child3 = Tag.new(name: 'Third Child')
    parent.add_child child3
    
    # Create children via parent assignment
    Tag.create(name: 'Fourth Child', parent: parent)
    
    # Accessing data
    grandparent.self_and_descendants.collect(&:name)
    #=> ["Grandparent", "Parent", "First Child", "Second Child", "Third Child", "Fourth Child"]
    
    child1.ancestry_path
    #=> ["Grandparent", "Parent", "First Child"]
  8. Implement Polymorphic hierarchies with STI

    master

    Single Table Inheritance (STI) is supported. Add has_closure_tree to the base class.

    Warning: Calling rebuild! on a subclass only rebuilds the hierarchy for that specific subclass, which may leave other subclasses without hierarchy entries. To rebuild the entire hierarchy including all subclasses, override rebuild! in the subclasses to call the base class method.

    class Tag < ApplicationRecord
      has_closure_tree
    end
    
    class WhenTag < Tag ; end
    class WhereTag < Tag ; end
    
    # To ensure full hierarchy rebuild:
    class WhenTag < Tag
      def self.rebuild!
        Tag.rebuild!
      end
    end
  9. Run Closure Tree tests with different databases

    master

    By default, rake test runs using SQLite. To run the test suite against a different database (like PostgreSQL or MySQL), provide the DATABASE_URL environment variable.

    $ DATABASE_URL=postgres://localhost/my_database rake test 
  10. Configure `has_closure_tree` options

    master

    When including has_closure_tree in your ActiveRecord model, you can pass an options hash to customize the tree behavior.

    Key options include:

    • :parent_column_name: The foreign key column for the parent (defaults to "parent_id").
    • :hierarchy_class_name: The name of the hierarchy class (defaults to ModelName + "Hierarchy").
    • :hierarchy_table_name: The name of the hierarchy table (defaults to model_name_hierarchies).
    • :dependent: Defines behavior when a node is destroyed:
      • :nullify (default): Sets parent column to null; children become roots.
      • :adopt: Moves children to the grandparent.
      • :delete_all: Deletes all descendants (skips destroy hooks).
      • :destroy: Destroys all descendants (runs destroy hooks).
      • nil: Does nothing to descendants.
    • :name_column: The column used by path-based lookup methods (e.g., find_by_path).
    • :order: Sets up deterministic ordering.
    • :scope: Restricts root nodes and sibling ordering to specific columns (e.g., scope: :user_id or scope: [:user_id, :group_id]).
    • :touch: Cascades touch calls to all children.
    • :advisory_lock_timeout_seconds: Sets a timeout for the advisory lock; raises WithAdvisoryLock::FailedToAcquireLock if the timeout is reached.
  11. Scope root nodes and sibling ordering

    master

    When using numeric_order: true, root nodes are assigned global order values by default. To prevent performance issues in multi-tree databases or to isolate trees, use the scope option. This ensures that root nodes, sibling reordering, and child reordering are all constrained by the specified column(s).

    You can also disable root ordering entirely using dont_order_roots: true, though this will cause prepend_sibling, append_sibling, and roots_and_descendants_preordered to raise a RootOrderingDisabledError.

    # Scope by a single column
    class Block < ApplicationRecord
      has_closure_tree order: 'sort_order', numeric_order: true, scope: :user_id
    end
    
    # Scope by multiple columns
    class Block < ApplicationRecord
      has_closure_tree order: 'sort_order', numeric_order: true, scope: [:user_id, :group_id]
    end
    
    # Disable root ordering
    class Block < ApplicationRecord
      has_closure_tree order: 'sort_order', numeric_order: true, dont_order_roots: true
    end
  12. Customize Closure Tree error messages with I18n

    master

    You can customize the error messages used by Closure Tree (such as loop_error) by adding keys to your application's I18n configuration files.

    en-US:
      closure_tree:
        loop_error: Your descendant cannot be your parent!