Table of contents

Categories in Django using ForeignKey to self

Categories in Django using ForeignKey to self

Grouping by categories is quite common in web applications, from movies, courses or any other resource that presents a hierarchical relationship to another object. In Django there are different ways to model these relationships. Probably, the first that will come to your mind will be to create a category object, and then relate it by means of a ForeignKey with a subcategory.

One subcategory or level per model

What I meant by one category or level per model is something like this:

# app/models.py
from django.db import models

class Category(models.Model):
    name = models.CharField(max_length=256)
    # otras propiedades

class SubCategory(models.Model):
    name = models.CharField(max_length=256)
    # otras propiedades
    category = models.ForeignKey(Category, related_name="subcategories", blank=True, null=True, on_delete=models.CASCADE)

This approach to the hierarchy problem in Django looks good at first glance. The resulting structure will look something like this:

Subcategory model schema with ForeignKey made Category in Django

The problem of using a model by category

This scheme will work in situations where the hierarchies do not nest very deep, but what if those subcategories have subcategories?

Imagine a horror movie category, with a ghost subcategory that, in turn, has a home ghost subcategory and this, in turn, a subcategory of type of ending.

Well, we add a SubSubCategory class, don’t we? But… what if those SubSubCategories have in turn subcategories. See what I’m trying to get at?

Infinite subcategories problem scheme

Every time you need to create a new subcategory you will have to create a new model in the models.py file of your application. And not only that, but a new table that probably only has a few records. Is there a better approach to the problem? The versatile Django ORM offers a pretty clean solution.

ForeignKey to the same model in Django

To simplify the problem of categories in Django, we create a single model, with a property of type ForeignKey or foreign key pointing to the same object; that is, to self.

# app/models.py
from django.db import models

class Category(models.Model):
    name = models.CharField(max_length=256)
    parent = models.ForeignKey(
        "self",
        related_name="subcategories",
        on_delete=models.CASCADE,
        blank=True,
        null=True,
    )

In this way we will have a structure similar to a graph, where each node points to another.

Schematic diagram of how ForeignKey made self(the same model) works in Django

This new arrangement allows us to create as many subcategories as we want, without the need to create new models. To do so, we simply assign the parent property to the Category class instance we want it to belong to.

Accessing the same model in Django

See how the ForeignKey does self in practice:

from my_app.models import Category
categoria_padre = Category.objects.create(name="Lenguajes de Programacion")
subcategoria = Category.objects.create(name="Python")
subcategoria.parent = categoria_padre
# Guardamos en la base de datos
subcategoria.save()

I explain what happens. First we create a main category, and a subcategory. Later, we assign the property parent, of this last one, to the main category. And ready, we save.

Finally, let’s create a sub-subcategory for our subcategory

subsubcategoria = Category.objects.create(name="Django")
subsubcategoria.parent = subcategoria
subsubcategoria.save()

Notice how we have created another sub-subcategory (called Django), which comes from the same model we used in the other two. To assign it to a category, we match its parent property to the subcategory we created (named Python).

As you have already seen, a single model allows us to add as many subcategories as we want.

Access to subcategories

If we examine the parent category, which we created previously, we will notice that it has a list of subcategories, among which our subcategory (Call Python) is already included.

We can access it as we would any other many-to-one relationship.

categoria_padre.subcategories.all()
<QuerySet [<Category: Category object (2)>]>
categoria_padre.subcategories.all()[0].name
'Python'

Go from subcategories to categories

On the other hand, if we want to go “in reverse” from the most nested category to the least nested, we simply go through it; we access the parent, and then the parent of the next instance, and so on as many nestings as we need.

subsubcategoria.name
'Django'
subsubcategoria.parent.name
'Python'
subsubcategoria.parent.parent.name
'Lenguajes de Programacion'

Other resources

Eduardo Zepeda
Web developer and GNU/Linux enthusiast always learning something new. I believe in choosing the right tool for the job and that simplicity is the ultimate sophistication. I'm under the impression that being perfect is the enemy of getting things done. I also believe in the goodnesses of cryptocurrencies outside of monetary speculation.
Read more