Django Tree Queries
django-tree-queries is a Django library that enables efficient querying of hierarchical data structures (trees) using adjacency lists and recursive Common Table Expressions (CTEs). It provides a lightweight solution for managing tree-like models within the Django ORM, focusing on explicit opt-in for tree-specific features rather than extensive configurability. The library is actively maintained, with version 0.24.0 currently available, supporting modern Django and Python versions.
Common errors
-
FieldError: Cannot resolve keyword 'tree_depth' into field.
cause Attempting to access `tree_depth` or other tree-specific fields without calling `.with_tree_fields()` on the queryset. These fields are dynamic annotations, not stored model fields.fixAdd `.with_tree_fields()` to your queryset before accessing `tree_depth`, `tree_path`, or `tree_ordering`. Example: `MyModel.objects.with_tree_fields().first().tree_depth`. -
AttributeError: 'MyModel' object has no attribute 'ancestors' (or 'descendants', etc.)
cause The model instance was retrieved without using the `TreeQuerySet` methods, or the model does not inherit from `TreeNode`. Standard Django model instances do not inherently have these tree methods.fixEnsure your model inherits from `tree_queries.models.TreeNode` and retrieve instances using `MyModel.objects` (which is a `TreeQuerySet`) or explicitly use `TreeQuerySet.as_manager()` for custom managers. Example: `node = MyModel.objects.get(pk=1); ancestors = node.ancestors()`. -
django.db.utils.ProgrammingError: subquery must return only one column
cause This error can occur when using complex subqueries or aggregations with tree querysets, especially when trying to aggregate values bottom-up (from descendants to ancestors), which is difficult or impossible with the library's top-down CTE approach.fixFor complex aggregations, particularly bottom-up, consider loading smaller trees into Python memory and performing calculations there, or restructuring your query to align with the top-down nature of the CTE.
Warnings
- breaking Django's standard `order_by()` method is not supported for tree ordering; nodes are returned according to a depth-first search. Use `order_siblings_by("field_name")` instead.
- gotcha The `tree_depth`, `tree_path`, and `tree_ordering` fields are only available on querysets where `with_tree_fields()` has been explicitly called. They are not stored in the database or available on newly created/saved instances.
- gotcha Performance can degrade significantly on very large tables or deep trees, especially when queries involve filtering or aggregating parts of the tree, as the recursive CTE might calculate all trees in the table before filtering.
- gotcha The `parent` foreign key field in models must explicitly be named `parent` for `TreeNode` to function correctly.
- gotcha The internal representation of `tree_path` and `tree_ordering` can change in future versions and should not be relied upon for application logic, especially for non-PostgreSQL databases where `tree_path` is a string representation.
- gotcha MySQL and MariaDB databases have a maximum tree depth limit of 50 levels due to their lack of native array support, which `django-tree-queries` uses for `tree_path` and `tree_ordering` internally.
Install
-
pip install django-tree-queries
Imports
- TreeNode
from django.db import models; class MyModel(models.Model): ...
from tree_queries.models import TreeNode
- TreeQuerySet
from django.db.models import QuerySet
from tree_queries.query import TreeQuerySet
- TreeNodeForeignKey
from tree_queries.fields import TreeNodeForeignKey
Quickstart
import os
from django.db import models
# Assuming 'myapp' is in INSTALLED_APPS
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
import django
django.setup()
from tree_queries.models import TreeNode
class Category(TreeNode):
name = models.CharField(max_length=255)
class Meta:
app_label = 'myapp'
def __str__(self):
return self.name
# Example Usage (after makemigrations and migrate)
# from myapp.models import Category
# root = Category.objects.create(name='Electronics')
# child1 = Category.objects.create(name='Smartphones', parent=root)
# child2 = Category.objects.create(name='Laptops', parent=root)
# grandchild = Category.objects.create(name='Gaming Laptops', parent=child2)
# Retrieve a tree with additional fields
# for node in Category.objects.with_tree_fields().order_siblings_by('name'):
# print(f"{'--' * node.tree_depth} {node.name} (depth: {node.tree_depth})")