A Django implementation for PostgreSQL's ltree extension, providing efficient storage and querying of hierarchical tree-like data.
See PostgreSQL's ltree documentation to learn more about it.
The main benefits of ltree:
- Efficient path queries (ancestors, descendants, pattern matching)
- Index-friendly hierarchical storage
- Powerful label path searching
- Native PostgreSQL performance for tree operations
- Django model fields for ltree data types
- Query utilities for common tree operations
- Migration support for ltree extension installation
- Compatibility with Django's ORM and query syntax
- Django 5.2+
- Python 3.11+
- PostgreSQL 14+ (with ltree extension enabled)
-
Install the package:
pip install django-ltree
-
Add to your
INSTALLED_APPS:INSTALLED_APPS = [ ... "django_ltree", ... ]
-
Run migrations to install the ltree extension:
python manage.py migrate django_ltree
django-ltree provides a base model class called TreeModel.
TreeModel does these things out of the box:
- adds a field called
pathto your model (by default, path is created by items Id plus parent's path) - adds
t_objectswhich is theTreeManageryou can use to work with tree data - adds two indexes for
path(oneBTreeIndex, oneGistIndex) - orders items base on
path
if you are overriding the Meta class of your model, you may want to inherit from TreeModel.Meta.
class Meta(TreeModel.Meta):to keep the indexes and ordering.
-
inherit from TreeModel:
from django_ltree.models import TreeModel class Category(TreeModel): name = models.CharField(max_length=50)
-
Create tree nodes:
# make an item without a parent (root) root = Category.t_objects.create(name="Root") # make a child item child = Category.t_objects.create_child(name="Child", parent=root) # you can also use `add_child` directly on root child2 = root.add_child(name="another child")
note that path is handled by django-ltree, you don't need to pass any value for it
-
Query ancestors and descendants:
# Get all ancestors child.ancestors() # Get all descendants child.descendants()
paths are made using the objects id and (if exists) it's parent's path.
if you need to use a different field for path generation, configure it like this:
class Role(TreeModel):
name = CharField()
t_objects = TreeManager(path_field="name")now paths are created using the name field
su = Role.t_objects.create(name="SuperUser")
print(su.path) # PathField("SuperUser")
admin = su.add_child(name="Admin")
print(admin.path) # PathField("SuperUser.Admin")when using an alternative field for path generation, it is recommended to use a field that ensures uniqueness to avoid confilicts.
if you are using a field that is not auto-generated (like name in the example
above), it is recommended to overwrite TreeManager.create and
TreeManager.create_child like this:
class MyTreeManager(TreeManager):
def create(self, **kwargs):
"""create an item with no parents (root)"""
kwargs["path"] = PathValue([kwargs[self.path_field]])
obj = self._create(**kwargs)
return obj
def create_child(self, parent: "TreeModel | PathValue | None" = None, **kwargs):
if not parent:
return self.create(**kwargs)
prefix = parent.path if isinstance(parent, models.Model) else parent
kwargs["path"] = PathValue([*prefix, kwargs[self.path_field]])
obj = self._create(**kwargs)
return objfor slightly better performance and less overhead.
this does not work for auto-generated fields like id.
TreeModel has the following methods:
-
label(self): returns the last part ofpath -
ancestors(self): return all the ancestors of the current item -
descendants(self): return all the descendants of the current item -
parent(self): return the immediate parent of the current item -
get_root(self): return the root parent of this item -
children(self): return all the immediate children of the current item -
siblings(self): return all the siblings of the current item (items that share the same parent with this item) -
add_child(self, **kwargs): create a child for this item kwargs are the arguments used to make the child (the model fields) -
change_parent(self, new_parent): change the parent of the current item (this moves the item and all it's descendants to be under another item) new_parent is either a object of the same model, or thepathvalue of an object -
make_root(self): move the current item to be a root item (moves the item and all it's descendants) -
delete(self, cascade=False, **kwargs): deletes the current item if cascade is True, all the descendants are also deleted, otherwise they will move to become the descendants of the first parent of the deleted item -
delete_cascade(self, **kwargs): delete the current item and all it's children
TreeManager has the following methods
-
create_child(self, parent=None, **kwargs): creates an item ifparentis provided, it will become the parent item of the created item, otherwise creation will happen as rootkwargsare the model fields used to create the item -
create(self, **kwargs): create a root itemkwargsare the model fields used to create the item -
roots(self): return all the root items from database -
children(self, path): return all the children of the specifiedpath
for a list of all available operations and functions for ltree check https://www.postgresql.org/docs/current/ltree.html#LTREE-OPS-FUNCS
-
exact(same as=in postgresql)TreeModel.t_objects.filter(path__exact=path) -
ancestors(same as@>in postgresql)TreeModel.t_objects.filter(path__ancestors=path) -
descendants(same as<@in postgresql)TreeModel.t_objects.filter(path__descendants=path) -
match(same as~in postgresql)TreeModel.t_objects.filter(path__match=f"{self.path}.*{{1}}") -
contains(same as?in postgresql)TreeModel.t_objects.filter(path__contains="1.*") -
depth(callsNLEVELfunction from postgresql)TreeModel.t_objects.filter(path__depth=len(path) + 1)
-
django_ltree.functions.NLevelsame as NLEVEL function from postgresql -
django_ltree.functions.Subpathsame asSUBPATHfunctions from postgresql
for concatenation (||) you can use django.db.models.functions.Concat
- Since PostgreSQL 16, the
-character is also permitted. The tests in this repository expect version 16 or higher. If you're using PostgreSQL 15 or earlier, make sure to exclude-from labels.
For complete documentation, see [TODO: Add Documentation Link].
- Source Code: https://github.com/mariocesar/django-ltree
- Bug Reports: https://github.com/mariocesar/django-ltree/issues
- PyPI Package: https://pypi.org/project/django-ltree/
- PostgreSQL ltree Docs: https://www.postgresql.org/docs/current/ltree.html
Contributions are welcome! Please see CONTRIBUTING.md for guidelines.