Hierarchical Navigations in Django (Treebeard)
django python
Hierarchical data—categories, org charts, nested comments—is everywhere. SQL wasn’t designed for it, but django-treebeard handles it elegantly.
The Problem
Naive approach:
class Category(models.Model):
name = models.CharField(max_length=100)
parent = models.ForeignKey('self', null=True, blank=True, on_delete=models.CASCADE)
Getting all descendants requires recursive queries:
def get_descendants(category):
"""N+1 nightmare"""
descendants = []
for child in category.category_set.all():
descendants.append(child)
descendants.extend(get_descendants(child))
return descendants
For a tree with 1000 nodes, this is hundreds of queries.
Treebeard’s Approach
django-treebeard implements three algorithms:
| Algorithm | Reads | Writes | Best For |
|---|---|---|---|
| Materialized Path | Fast | Fast | General use |
| Nested Sets | Fastest | Slow | Read-heavy |
| Adjacency List | Moderate | Fast | Write-heavy |
Setup
pip install django-treebeard
# settings.py
INSTALLED_APPS = [
...
'treebeard',
]
Materialized Path (Recommended)
from treebeard.mp_tree import MP_Node
class Category(MP_Node):
name = models.CharField(max_length=100)
node_order_by = ['name']
def __str__(self):
return self.name
Each node stores its path:
ID | Path | Depth | Name
1 | 0001 | 1 | Electronics
2 | 00010001 | 2 | Laptops
3 | 000100010001 | 3 | Gaming Laptops
4 | 00010002 | 2 | Phones
Basic Operations
# Create root node
electronics = Category.add_root(name='Electronics')
# Add children
laptops = electronics.add_child(name='Laptops')
phones = electronics.add_child(name='Phones')
# Add grandchild
gaming = laptops.add_child(name='Gaming Laptops')
# Get all descendants in ONE query
descendants = electronics.get_descendants()
# Get ancestors
ancestors = gaming.get_ancestors()
# Get siblings
siblings = laptops.get_siblings()
# Get children
children = electronics.get_children()
Efficient Queries
# Get full tree (one query)
tree = Category.get_tree()
# Get tree as nested dict
tree_dict = Category.dump_bulk()
# Check if ancestor
is_ancestor = electronics.is_ancestor_of(gaming) # True
# Get depth
depth = gaming.get_depth() # 3
Moving Nodes
# Move node to new parent
gaming.move(phones, pos='first-child')
# Move as sibling
laptops.move(phones, pos='right')
Admin Integration
from django.contrib import admin
from treebeard.admin import TreeAdmin
from treebeard.forms import movenodeform_factory
from .models import Category
@admin.register(Category)
class CategoryAdmin(TreeAdmin):
form = movenodeform_factory(Category)
list_display = ['name', 'depth']
This gives you a drag-and-drop tree interface.
Building Navigation
# views.py
def navigation(request):
# Get tree with custom annotation
categories = Category.get_tree().annotate(
product_count=Count('products')
)
return render(request, 'nav.html', {'categories': categories})
<!-- nav.html -->
<ul class="nav-tree">
{% for category in categories %}
{% if category.depth == 1 %}
<li>
<a href="{% url 'category' category.pk %}">
{{ category.name }} ({{ category.product_count }})
</a>
{% elif category.depth > prev_depth %}
<ul>
<li>
<a href="{% url 'category' category.pk %}">{{ category.name }}</a>
{% elif category.depth == prev_depth %}
</li>
<li>
<a href="{% url 'category' category.pk %}">{{ category.name }}</a>
{% else %}
{% for i in "x"|rjust:depth_diff %}
</li></ul>
{% endfor %}
</li>
<li>
<a href="{% url 'category' category.pk %}">{{ category.name }}</a>
{% endif %}
{% with category.depth as prev_depth %}{% endwith %}
{% endfor %}
</ul>
Recursive Serialization
# serializers.py
class CategorySerializer(serializers.ModelSerializer):
children = serializers.SerializerMethodField()
class Meta:
model = Category
fields = ['id', 'name', 'children']
def get_children(self, obj):
children = obj.get_children()
return CategorySerializer(children, many=True).data
# Or build tree efficiently
def serialize_tree(root):
"""Serialize tree in single traversal."""
nodes = root.get_descendants()
nodes_map = {n.pk: {'id': n.pk, 'name': n.name, 'children': []}
for n in nodes}
nodes_map[root.pk] = {'id': root.pk, 'name': root.name, 'children': []}
for node in nodes:
parent_path = node.path[:-4] # Materialized path parent
parent = next((n for n in nodes if n.path == parent_path), root)
nodes_map[parent.pk]['children'].append(nodes_map[node.pk])
return nodes_map[root.pk]
Common Patterns
Breadcrumbs
def product_detail(request, pk):
product = get_object_or_404(Product, pk=pk)
breadcrumbs = product.category.get_ancestors(include_self=True)
return render(request, 'product.html', {
'product': product,
'breadcrumbs': breadcrumbs
})
<nav class="breadcrumb">
<a href="/">Home</a>
{% for crumb in breadcrumbs %}
› <a href="{% url 'category' crumb.pk %}">{{ crumb.name }}</a>
{% endfor %}
</nav>
Category Products with Descendants
def category_products(request, pk):
category = get_object_or_404(Category, pk=pk)
# Get products from this category and all descendants
descendant_ids = category.get_descendants().values_list('id', flat=True)
all_category_ids = list(descendant_ids) + [category.id]
products = Product.objects.filter(category_id__in=all_category_ids)
return render(request, 'products.html', {
'category': category,
'products': products
})
Performance Considerations
Indexing
class Category(MP_Node):
name = models.CharField(max_length=100, db_index=True)
class Meta:
indexes = [
models.Index(fields=['path']),
models.Index(fields=['depth']),
]
Prefetching
# This doesn't work well with tree structures
# Category.objects.prefetch_related('children')
# Instead, get full tree and organize in memory
tree = list(Category.get_tree())
Final Thoughts
Hierarchical data is common. django-treebeard handles it properly:
- Efficient tree traversal
- Single-query operations
- Clean API
Use Materialized Path for most cases. Your N+1 problems disappear.
Trees belong in databases, not just file systems.