After writing up a method of using a Postgres View that generates a materialised path within the context of a Django model, I came across some queries of my data that were getting rather troublesome to write. It occurred to me that having a closure table would be useful. Specifically, I needed all of the descendants of a given set of nodes.
I couldn’t find an existing Postgres extension that will manage the closure table, and didn’t feel like writing my own implemention using triggers just yet. However, it occurred to me that I could use a similar trick to the recursive materialised path view. Thus, we have a Closure View.
We will start with the Django models:
class Node(models.Model): node_id = models.AutoField(primary_key=True) parent = models.ForeignKey('tree.Node', related_name='children', null=True, blank=True) descendants = models.ManyToManyField('tree.Node', related_name='ancestors', through='tree.Closure') class Meta: app_label = 'tree' class Closure(models.Model): path = ArrayField(base_field=models.IntegerField(), primary_key=True) ancestor = models.ForeignKey('tree.Node', related_name='+') descendant = models.ForeignKey('tree.Node', related_name='+') depth = models.IntegerField() class Meta: app_label = 'tree' managed = False
You may notice I have a
path column. I’m using this for the primary key, and it may turn out to be useful later.
Let’s have a look at the View:
CREATE RECURSIVE VIEW tree_closure(path, ancestor_id, descendant_id, depth) AS SELECT ARRAY[node_id], node_id, node_id, 0 FROM tree_node UNION ALL SELECT parent_id || path, parent_id, descendant_id, depth + 1 FROM tree_node INNER JOIN tree_closure ON (ancestor_id = node_id) WHERE parent_id IS NOT NULL;
This uses a recursive query. The first part builds the self-reference relations, and the second part uses the
RECURSIVE function to collect child nodes for each node already in the table (or added in previous iterations of this part of the view).
Now, because we are using the in-built Django Many to Many features, we have some nice queries ready to go:
node.ancestors.all(): All ancestors of a given Node instance.
node.descendants.all(): All descendants of a given Node instance.
Node.objects.filter(ancestors=queryset): All descendants of all nodes in a queryset.
Node.objects.filter(descendants=queryset): All ancestors of all nodes in a queryset.
Of particular note are the bottom two: these are rather cumbersome to write in older versions of Django.