django-tree-queries

repository·main·Indexed 19 days ago

https://github.com/feincms/django-tree-queries

A Django application for efficient querying of hierarchical tree structures using adjacency lists and recursive Common Table Expressions (CTEs). It provides TreeNode and OrderableTreeNode models, a specialized TreeQuerySet for depth-first search traversal, and template tags for recursive rendering. Supported databases include PostgreSQL, sqlite3 (3.8.3+), MariaDB (10.2.2+), and MySQL (8.0+). It supports integer and UUID primary keys and includes an optional TreeAdmin for drag-and-drop management in the Django Admin.

Tokens
2.9K
Snippets
13
Records
16
Agent score
18%

What's inside django-tree-queries

  1. Understand tree fields (tree_depth, tree_path, tree_ordering)

    main

    When using .with_tree_fields(), each node is annotated with:

    • tree_depth: Integer representing depth (root is 0).
    • tree_path: Array of primary keys from root to the current node.
    • tree_ordering: Array of the ordering/ranking values used for sibling ordering at each level.

    Note: Tree fields are calculated at query time via a recursive CTE and are NOT stored in the database. They are unavailable immediately after .create(), .save(), or .refresh_from_db(). You must re-query using .with_tree_fields() to access them.

    # Correct way to access tree fields after creation
    new_node = Node.objects.create(name="New Child", parent=parent_node)
    new_node = Node.objects.with_tree_fields().get(pk=new_node.pk)
    print(new_node.tree_depth)
  2. Understand the tree fields provided by the CTE

    main

    When using with_tree_fields(), the following fields are added to your queryset results via the CTE:

    • tree_depth: An integer representing the depth of the node (root nodes have a depth of zero).
    • tree_path: An array of primary keys representing the path from the root to the current node (including the node itself).
    • tree_ordering: An array of values used for ordering nodes within their siblings at each level of the hierarchy.

    Warning: The contents of tree_path and tree_ordering are subject to change and should not be relied upon for business logic.

  3. Implement a basic tree node

    main

    To create a tree structure, extend tree_queries.models.TreeNode. This abstract model automatically includes a parent foreign key and uses model validation to help prevent infinite loops in your tree structure.

    Note that the library currently only supports integer and UUID primary keys.

    from tree_queries.models import TreeNode
    
    class MyNode(TreeNode):
        # Your custom fields here
        pass
  4. Render trees in Django templates

    main

    Use the provided template tags to render tree structures efficiently without extra database queries.

    Setup:

    1. Add 'tree_queries' to INSTALLED_APPS.
    2. Load tags in your template: {% load tree_queries %}.

    Tags:

    • {% recursetree queryset %}: Recursively renders nodes. Provides node, children, and is_leaf context variables.
    • {% for node, structure in nodes|tree_info %}: A filter that provides structure info including new_level (boolean), closed_levels (list), and ancestors (list).
    {% load tree_queries %}
    <ul>
    {% recursetree nodes %}
        <li>
            {{ node.name }}
            {% if children %}<ul>{{ children }}</ul>{% endif %}
        </li>
    {% endrecursetree %}
    </ul>
  5. Configure TreeAdmin for Django Admin

    main

    To use the drag-and-drop style tree management in Django Admin, install the admin extra and use the TreeAdmin class.

    Installation:

    pip install django-tree-queries[admin]

    Usage: Register your model with TreeAdmin and specify the position_field if you are using manual or automatic ordering.

    from django.contrib import admin
    from tree_queries.admin import TreeAdmin
    from tree_queries.models import OrderableTreeNode
    
    class Category(OrderableTreeNode):
        name = models.CharField(max_length=100)
    
    @admin.register(Category)
    class CategoryAdmin(TreeAdmin):
        list_display = [*TreeAdmin.list_display, "name"]
        position_field = "position"
  6. Use TreeQuerySet methods for tree data

    main

    To access tree-specific data and hierarchy logic, you must use tree_queries.query.TreeQuerySet.

    Key methods include:

    • with_tree_fields(): Enables the Common Table Expression (CTE) to add tree-related fields to your query results.
    • order_siblings_by("field_name"): Orders nodes within their sibling groups using a specific model field. Note that standard Django order_by() is not supported for tree traversal; nodes are returned using a depth-first search algorithm.
    • tree_filter() and tree_exclude(): Optimized methods for filtering ancestors or descendants. These filter the base table before building the tree structure, which is significantly more performant for large tables.
    • tree_fields(): Aggregates ancestor field values into arrays.
  7. Query tree structures and ancestors/descendants

    main

    Use the following methods on your tree queryset to navigate the hierarchy:

    • with_tree_fields(): Fetches nodes in depth-first search order and attaches tree_depth, tree_path, and tree_ordering to each object.
    • ancestors(include_self=True): Fetches all ancestors starting from the root.
    • descendants(include_self=True): Fetches all descendants in depth-first order, including the node itself.
    • order_siblings_by("field"): Temporarily overrides sibling ordering by a specific field.
    • without_tree_fields(): Removes tree annotations to improve performance when they are no longer needed.
    # Fetch nodes in depth-first search order with tree attributes
    nodes = Node.objects.with_tree_fields()
    
    # Fetch ancestors
    ancestors = node.ancestors(include_self=True)
    
    # Fetch descendants
    descendants = node.descendants(include_self=True)
    
    # Revert to standard queryset
    nodes = Node.objects.with_tree_fields().without_tree_fields()
  8. Define a basic tree node

    main

    To create a simple tree structure without sibling ordering, extend TreeNode. This model includes utilities and validation to prevent loops in the tree structure.

    from tree_queries.models import TreeNode
    from django.db import models
    
    class Node(TreeNode):
        name = models.CharField(max_length=100)
  9. Filter tree subsets efficiently with tree_filter()

    main

    For large tables, use tree_filter() or tree_exclude() instead of standard .filter(). These methods apply filters to the base table before the recursive CTE runs, which significantly improves performance by limiting the scope of the tree traversal.

    # Get a specific tree from a forest
    product_tree = Node.objects.with_tree_fields().tree_filter(category="products")
    
    # Exclude specific sections
    content_trees = Node.objects.with_tree_fields().tree_exclude(category="archived")
  10. Configure a default Tree Manager with tree fields

    main

    If you want every query from your model's manager to include tree fields (like tree_depth, tree_path, and tree_ordering) by default, use TreeQuerySet.as_manager(with_tree_fields=True).

    from tree_queries.models import TreeNode
    from tree_queries.query import TreeQuerySet
    
    class MyNode(TreeNode):
        objects = TreeQuerySet.as_manager(with_tree_fields=True)
  11. Aggregate ancestor fields with tree_fields()

    main

    Use .tree_fields() to collect values from all ancestors (including the current node) into an array. This is useful for building breadcrumbs or inheriting permissions/categories down the tree.

    # Aggregate names from all ancestors into an array
    nodes = Node.objects.with_tree_fields().tree_fields(tree_names="name")
    
    for node in nodes:
        full_path = " > ".join(node.tree_names)