1

I have a model which looks like this:

class Category(models.Model):
    name = models.CharField(max_length=50)
    slug = models.SlugField()
    parent = models.ForeignKey(
        'categories.Category',
        null=True,
        blank=True,
        on_delete=models.CASCADE,
        related_name='categories'
    )

basically, in the parent field, it references itself. If a parent is set to None, it's the root category.

I use it to build a hierarchy of categories.

What would be the most efficient way to:

  1. fetch all the objects through the hierarchy
  2. display them in a template?

For some reason, select_related does not seem to lead to performance improvements here.

I also found this: How to recursively query in django efficiently?

But had a really hard time applying it to my example, because I still don't really understand what's going on. This was my result:

    WITH RECURSIVE hierarchy(slug, parent_id) AS (
        SELECT slug, parent_id 
        FROM categories_category
        WHERE parent_id = '18000'

        UNION ALL

        SELECT sm.slug, sm.parent_id
        FROM categories_category AS sm, hierarchy AS h
        WHERE sm.parent_id = h.slug
        )
    SELECT * FROM hierarchy

Would appreciate any help.

Thanks!

OhMad
  • 6,871
  • 20
  • 56
  • 85
  • If I knew the exact problem, I probably would have solved it :) Really, I adapted the code but it didn't work as expected. The main reason being, I didn't understand it... – OhMad Dec 02 '22 at 10:47
  • 1
    As mentioned by you recursive common table expressions are a way to solve this in Postgres, but another approach would be to loon into [django-mptt](https://django-mptt.readthedocs.io/en/latest/). Maybe have a look at some overviews on how to efficiently implement tree queries in Django https://406.ch/writing/django-tree-queries/ – Bernhard Vallant Dec 05 '22 at 10:53
  • @OhMad Your [recursive](https://www.postgresql.org/docs/current/queries-with.html#QUERIES-WITH-RECURSIVE) cte seems fine, assuming you keep the id as slug, and parent_id points at the parent's slug, and that you only want to go up the tree from the target element - which I'm not sure why you're specifying based on it's parent instead of it's own unique identifier. Apart from the child elements, what do you find insufficient about it? – Zegarek Dec 07 '22 at 14:47
  • Fastest way requires a bit modification on table, based on "materialized path trees" then no CTE needed to retrieve hierarchy. This is what django-treebeard uses. Its simple and easy to understand Check this url https://django-mptt.readthedocs.io/en/latest/overview.html#what-is-modified-preorder-tree-traversal – bekir çelik Dec 08 '22 at 11:46

2 Answers2

0

One possible solution can be using https://django-mptt.readthedocs.io/en/latest/overview.html#what-is-django-mptt

MPTT is a technique for storing hierarchical data in a database. The aim is to make retrieval operations very efficient. The trade-off for this efficiency is that performing inserts and moving items around the tree is more involved, as there’s some extra work required to keep the tree structure in a good state at all times.

from django.db import models
from mptt.models import MPTTModel, TreeForeignKey

class Category(MPTTModel):
    name = models.CharField(max_length=50)
    slug = models.SlugField()
    parent = TreeForeignKey(
        'self',
        null=True,
        blank=True,
        on_delete=models.CASCADE,
        related_name='children'
    )

    class MPTTMeta:
        order_insertion_by = ['name']

You can use the django-mptt template tag as this:

{% load mptt_tags %}
<ul>
    {% recursetree categories %}
        <li>
            {{ node.name }}
            {% if not node.is_leaf_node %}
                <ul class="children">
                    {{ children }}
                </ul>
            {% endif %}
        </li>
    {% endrecursetree %}
</ul>

There is a tutorial and more information in the library docs.

PDA
  • 26
  • 1
  • Hi there! Thanks, this worked. Any idea how I can filter the categories to only include categories (and all their parents) that are in use? Currently I have this: ctx['categories'] = Category.objects.filter(products__shop_id=self.shop.id).distinct() However, this only gives me the assigned level, and does not include the level 0, 1, and so on. – OhMad Dec 13 '22 at 15:05
0

I had the same problem and ended up creating the following function that hits the database once, then sorts out the heirarchy and returns a dict:

def get_category_tree():
    categories = Category.objects.order_by('name')
    itemtree = {}
    # Add 'children' attribute to each category; populate dict
    for category in categories:
        category.children = {}
        itemtree[category.pk] = category
    # Add categories to 'children'
    for key,value in itemtree.items():
        if value.parent_id:
            itemtree[value.parent_id].children[key] = value
    # Return top-level items
    return {k:v for k,v in itemtree.items() if not v.parent_id}

Each value of the returned dict is a top-level Category object which has a children attribute.

You can render it in the template by looping through the dict values. The following example will handle three levels of heirarchy:

<ul>
{% for level1 in category_tree.values %}
    <li>
        {{ level1.name }}
        {% if level1.children %}
        <ul>
            {for level2 in level1.children.values %}
            <li>{{ level2.name }}
                {% if level2.children %}
                <ul>
                    {for level3 in level2.children.values %}
                    <li>{{ level3.name }}</li>
                    {% endfor %}
                </ul>
                {% endif %}
            </li>
            {% endfor %}
        </ul>
        {% endif %}
    </li>
{% endfor %}
</ul>

If you need to render many levels of the heirarchy, you could consider using template recursion. Have a read of the following question and answers to determine if that might be suitable: Represent a tree of objects in Django template

MattRowbum
  • 2,162
  • 1
  • 15
  • 20