Skip to content

Commit 63afc1a

Browse files
authored
Merge pull request #35 from rcxwhiz/dev
2.0
2 parents bfb3f2e + c4c0e07 commit 63afc1a

File tree

17 files changed

+1465
-2743
lines changed

17 files changed

+1465
-2743
lines changed

README.md

Lines changed: 86 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,50 +6,103 @@
66
[![Supported Python versions](https://img.shields.io/pypi/pyversions/django-hierarchical-models.svg)](https://pypi.python.org/pypi/django-hierarchical-models/)
77
[![Supported Django versions](https://img.shields.io/pypi/djversions/django-hierarchical-models.svg)](https://pypi.python.org/pypi/django-hierarchical-models/)
88

9-
This package provides several implementations Django models which support hierarchical
10-
data. Efficiently modeling hierarchical, or tree like data, in a relational database
11-
can be non-trivial. The following models implement the same interface, but there have
12-
different tradeoffs.
9+
This package provides an abstract Django model which supports hierarchical data. The
10+
implementation is an adjacency list, which is rather naive, but actually has higher
11+
performance in this scenario than other implementations such as path enumeration or
12+
nested sets because those implementations store more data with each instance which must
13+
be updated before almost every operation, effectively doubling (or more) database
14+
queries and killing performance. The performance of this implementation actually holds
15+
up pretty well at large numbers of instances.
1316

14-
`models.HierarchicalModelInterface`
17+
## Usage
1518

16-
This abstract model defines the shared functionality of all hierarchical models. Some
17-
models may implement additional methods which are cheap for their implementation.
19+
```python
20+
from django.db import models
21+
from django_hierarchical_models.models import HierarchicalModel
1822

19-
`models.AdjacencyListModel`
23+
class MyModel(HierarchicalModel):
24+
name = models.CharField(max_length=100)
2025

21-
This is the most trivial implementation, using a single `_parent` foreign key field.
22-
Edits are efficient in this model, but queries for children/parents can be very
23-
expensive.
26+
...
2427

25-
`models.PathEnumerationModel`
28+
child = MyModel.objects.create(name="Betty")
29+
child.parent() # None
2630

27-
This model uses a `_ancestors` json field to store the path to its root. This model
28-
has middle ground efficiency for edits and queries. **NOTE:** This model requires
29-
database features that are not available in Oracle or SQLite backends. An exception
30-
will be raised if you attempt to use this model with an unsupported backend.
31+
parent = MyModel.objects.create(name="Simon")
32+
child.set_parent(parent)
33+
child.parent() # <MyModel: "Simon">
3134

32-
`models.NestedSetModel`
35+
child.root() # <MyModel: "Simon">
36+
parent.root() # <MyModel: "Simon">
3337

34-
This model uses `_left` and `_right` integer fields to determine which instances it is
35-
parent/child to. Queries can be very efficient in this model, but edits can be very
36-
expensive, possibly even requiring updates to `_left` and `_right` fields of ever model
37-
instance for a single edit.
38+
parent.direct_children() # [<MyModel: "Betty">]
3839

39-
`models.Node`
40+
child.is_child_of(parent) # True
41+
parent.is_child_of(child) # False
42+
```
4043

41-
Calls to `HierarchicalModel.children()` return this type, which has `instance` and
42-
`children` members, with the children being additional instances of `Node`.
44+
## Refreshing from database
4345

44-
#### Benchmarks
46+
External changes to an instance's parent are not automatically reflected in the
47+
instance. This leads to the following behavior:
4548

46-
These benchmarks are to illustrate the relative performance of the different models. As
47-
of now they are kind of whacked out. These tests were run with Postgres.
49+
```python
50+
instance_1 = MyModel.objects.create(name="Betty")
51+
instance_2 = MyModel.objects.create(parent=instance_1, name="Simon")
52+
insstance_2.parent() # <MyModel: "Betty">
4853

49-
| Model | Query Ancestors | Query Parent | Query Children | Query Direct Children | Create | Create Child | Delete | Delete Parent | Add Child | Remove Child | Set Parent | Set Parent Unchecked* |
50-
|------------------|-----------------|--------------|----------------|-----------------------|--------|--------------|--------|---------------|-----------|--------------|------------|-----------------------|
51-
| Adjacency List | 9.56 | 2.76 | 5.17 | 0.96 | 0.98 | 0.33 | 0.72 | 0.95 | 0.69 | 1.64 | 133.60 | 0.93 |
52-
| Path Enumeration | 9.17 | 2.67 | 29.71 | 0.84 | 0.99 | 0.32 | 0.75 | 1.26 | 0.94 | 1.79 | 2.98 | |
53-
| Nested Set | 169.86 | 118.97 | 211.75 | 107.30 | 3.31 | 59.19 | 113.79 | 175.11 | 71.13 | 293.61 | 572.47 | |
54+
instance_1.delete()
5455

55-
\* function only available on Adjacency List Model
56+
instance_2.parent() # <MyModel: "Betty">
57+
58+
instance_2.refresh_from_db()
59+
60+
instance_2.parent() # None
61+
```
62+
63+
```python
64+
instance_1 = MyModel.objects.create(name="Betty")
65+
instance_2 = MyModel.objects.create(parent=instance_1, name="Simon")
66+
instance_3 = MyModel.objects.create(parent=instance_2, name="Finn")
67+
instance_3_copy = MyModel.objects.get(pk=instance_3.pk)
68+
69+
instance_1.root() # <MyModel: "Betty">
70+
instance_2.root() # <MyModel: "Betty">
71+
instance_3.root() # <MyModel: "Betty">
72+
instance_3_copy.root() # <MyModel: "Betty">
73+
74+
instance_2.set_parent(None)
75+
76+
instance_1.root() # <MyModel: "Betty">
77+
instance_2.root() # <MyModel: "Simon">
78+
instance_3.root() # <MyModel: "Simon">
79+
instance_3_copy.root() # <MyModel: "Betty">
80+
81+
instance_3_copy.refresh_from_db()
82+
83+
instance_1.root() # <MyModel: "Betty">
84+
instance_2.root() # <MyModel: "Simon">
85+
instance_3.root() # <MyModel: "Simon">
86+
instance_3_copy.root() # <MyModel: "Simon">
87+
```
88+
89+
Moral of the story, if your instance's parent might have been edited/deleted,
90+
you will want to refresh your instance for that change to be reflected.
91+
92+
## Benchmarks
93+
94+
The following benchmarks demonstrate that the query performance of the model stays the
95+
same from 10,000 to 1,000,000 models. These tests were done with Postgres. The results
96+
are in the form `total time (s) / per instance (ms)`. Eventually the query performance
97+
of this model should scale down with the total number of instances in the database,
98+
but it appears up to these scales those effects are insignificant compared to other
99+
overhead.
100+
101+
| n | Chance Child | Query Parent | Query Root | Is Child Of | Query Ancestors | Query Direct Children | Query Children |
102+
|-----------|--------------|---------------|---------------|---------------|-----------------|-----------------------|----------------|
103+
| 10,000 | 50% | 0.29 / 0.029 | 0.27 / 0.027 | 0.27 / 0.027 | 0.29 / 0.029 | 0.78 / 0.078 | 3.85 / 0.385 |
104+
| 10,000 | 90% | 0.30 / 0.030 | 0.39 / 0.039 | 0.31 / 0.031 | 0.30 / 0.030 | 0.87 / 0.087 | 5.07 / 0.507 |
105+
| 100,000 | 50% | 3.46 / 0.035 | 3.12 / 0.031 | 3.55 / 0.036 | 3.09 / 0.031 | 8.24 / 0.082 | 37.89 / 0.380 |
106+
| 100,000 | 90% | 4.10 / 0.041 | 3.48 / 0.035 | 3.88 / 0.039 | 3.55 / 0.036 | 8.89 / 0.089 | 48.30 / 0.483 |
107+
| 1,000,000 | 50% | 32.39 / 0.032 | 34.53 / 0.035 | 35.41 / 0.035 | 32.16 / 0.032 | 86.05 / 0.086 | 385.62 / 0.386 |
108+
| 1,000,000 | 90% | 34.87 / 0.035 | 38.59 / 0.039 | 38.93 / 0.039 | 36.51 / 0.037 | 87.49 / 0.087 | 490.65 / 0.491 |
Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
1-
from django_hierarchical_models.models.alm import AdjacencyListModel
2-
from django_hierarchical_models.models.interface import HierarchicalModelInterface
1+
from django_hierarchical_models.models.exceptions import CycleException
2+
from django_hierarchical_models.models.hierarchical_model import HierarchicalModel
33
from django_hierarchical_models.models.node import Node
4-
from django_hierarchical_models.models.nsm import NestedSetModel
5-
from django_hierarchical_models.models.pem import PathEnumerationModel
64

75
__all__ = (
8-
"AdjacencyListModel",
9-
"NestedSetModel",
10-
"PathEnumerationModel",
11-
"HierarchicalModelInterface",
6+
"HierarchicalModel",
127
"Node",
13-
"exceptions",
8+
"CycleException",
149
)

django_hierarchical_models/models/alm.py

Lines changed: 0 additions & 66 deletions
This file was deleted.

django_hierarchical_models/models/exceptions.py

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,14 @@
1-
class AlreadyHasParentException(Exception):
2-
def __init__(self, child, *args):
3-
super().__init__(*args)
4-
self.child = child
5-
6-
def __str__(self):
7-
return f"{self.child} already has a parent"
8-
9-
10-
class NotAChildException(Exception):
11-
def __init__(self, parent, child, *args):
12-
super().__init__(*args)
13-
self.parent = parent
14-
self.child = child
1+
class CycleException(Exception):
2+
"""Raised when setting a model parent would cause a cycle.
153
16-
def __str__(self):
17-
return f"{self.child} is not a child of {self.parent}"
4+
While representable, cycles can cause infinite loops in many of the
5+
HierarchicalModel methods.
186
7+
Attributes:
8+
parent: The instance which would have been the parent.
9+
child: The instance whose parent would have been updated.
10+
"""
1911

20-
class CycleException(Exception):
2112
def __init__(self, parent, child, *args):
2213
super().__init__(*args)
2314
self.parent = parent

0 commit comments

Comments
 (0)