Generic Functionality without Generic Relations
Tags: advanced, architecture, intermediate, django
Published: 2022-04-16 10:06:59

---
title: Generic Functionality without Generic Relations
published: true
date: 2022-04-08 22:06:00 UTC
tags: Advanced,Architecture,Intermediate,Django
canonical_url: http://djangotricks.blogspot.com/2022/04/generic-functionality-without-generic-relations.html
---

![Generic Functionality without Generic Relations](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gwk33avy4igf2dovgmhf.png)

When you have some generic functionality like anything commentable, likable, or upvotable, it’s common to use [Generic Relations in Django](https://simpleisbetterthancomplex.com/tutorial/2016/10/13/how-to-use-generic-relations.html). The problem with Generic Relations is that they create the relationships at the __application level__ instead of the __database level__, and that requires a lot of database queries if you want to aggregate content that shares the generic functionality. There is another way that I will show you in this article. 

I learned this technique at my first job in 2002 and then rediscovered it again with Django a few years ago. The trick is to have a generic `Item` model where every other autonomous model has a one-to-one relation to the `Item`. Moreover, the `Item` model has an `item_type` field, allowing you to recognize the backward one-to-one relationship.

Then whenever you need to have some generic categories, you link them to the `Item.` Whenever you create generic functionality like media gallery, comments, likes, or upvotes, you attach them to the `Item`. Whenever you need to work with permissions, publishing status, or workflows, you deal with the `Item`. Whenever you need to create a global search or trash bin, you work with the `Item` instances.

Let’s have a look at some code.

## Items

First, I'll create the `items` app with two models: the previously mentioned `Item` and the abstract model `ItemBase` with the one-to-one relation for various models to inherit:

```python
# items/models.py
import sys

from django.db import models
from django.apps import apps

if "makemigrations" in sys.argv:
    from django.utils.translation import gettext_noop as _
else:
    from django.utils.translation import gettext_lazy as _


class Item(models.Model):
    """
    A generic model for all autonomous models to link to.
    
    Currently these autonomous models are available:
    - content.Post
    - companies.Company
    - accounts.User
    """
    ITEM_TYPE_CHOICES = (
        ("content.Post", _("Post")),
        ("companies.Company", _("Company")),
        ("accounts.User", _("User")),
    )
    item_type = models.CharField(
        max_length=200, choices=ITEM_TYPE_CHOICES, editable=False, db_index=True
    )

    class Meta:
        verbose_name = _("Item")
        verbose_name_plural = _("Items")

    def __str__(self):
        content_object_title = (
            str(self.content_object) if self.content_object else "BROKEN REFERENCE"
        )
        return (
            f"{content_object_title} ({self.get_item_type_display()})"
        )

    @property
    def content_object(self):
        app_label, model_name = self.item_type.split(".")
        model = apps.get_model(app_label, model_name)
        return model.objects.filter(item=self).first()


class ItemBase(models.Model):
    """
    An abstract model for the autonomous models that will link to the Item.
    """
    item = models.OneToOneField(
        Item,
        verbose_name=_("Item"),
        editable=False,
        blank=True,
        null=True,
        on_delete=models.CASCADE,
        related_name="%(app_label)s_%(class)s",
    )

    class Meta:
        abstract = True

    def save(self, *args, **kwargs):
        if not self.item:
            model = type(self)
            item = Item.objects.create(
                item_type=f"{model._meta.app_label}.{model.__name__}"
            )
            self.item = item
        super().save()

    def delete(self, *args, **kwargs):
        if self.item:
            self.item.delete()
        super().delete(*args, **kwargs)
```

Then let's create some autonomous models that will have one-to-one relations with the `Item`. By "autonomous models," I mean those which are enough by themselves, such as posts, companies, or accounts. Models like types, categories, tags, or likes, wouldn't be autonomous.

## Posts

Second, I create the `content` app with the `Post` model. This model extends `ItemBase` which will create the one-to-one relation on save, and will define the `item_type` as `content.Post`: 

```python
# content/models.py
import sys

from django.contrib.auth.base_user import BaseUserManager
from django.db import models
from django.contrib.auth.models import AbstractUser

if "makemigrations" in sys.argv:
    from django.utils.translation import gettext_noop as _
else:
    from django.utils.translation import gettext_lazy as _

from items.models import ItemBase


class Post(ItemBase):
    title = models.CharField(_("Title"), max_length=255)
    slug = models.SlugField(_("Slug"), max_length=255)
    content = models.TextField(_("Content"))

    class Meta:
        verbose_name = _("Post")
        verbose_name_plural = _("Posts")

```

## Companies

Third, I create the `companies` app with the `Company` model. This model also extends `ItemBase` which will create the one-to-one relation on save, and will define the `item_type` as `companies.Company`: 


```python
# companies/models.py
import sys

from django.contrib.auth.base_user import BaseUserManager
from django.db import models
from django.contrib.auth.models import AbstractUser

if "makemigrations" in sys.argv:
    from django.utils.translation import gettext_noop as _
else:
    from django.utils.translation import gettext_lazy as _

from items.models import ItemBase


class Company(ItemBase):
    name = models.CharField(_("Name"), max_length=255)
    slug = models.SlugField(_("Slug"), max_length=255)
    description = models.TextField(_("Description"))

    class Meta:
        verbose_name = _("Company")
        verbose_name_plural = _("Companies")

```

## Accounts

Fourth, I'll have a more extensive example with the `accounts` app containing the `User` model. This model extends `AbstractUser` from `django.contrib.auth` as well as `ItemBase` for the one-to-one relation. The `item_type` set at the `Item` model will be `accounts.User`: 


```python
# accounts/models.py
import sys

from django.db import models
from django.contrib.auth.base_user import BaseUserManager
from django.contrib.auth.models import AbstractUser

if "makemigrations" in sys.argv:
    from django.utils.translation import gettext_noop as _
else:
    from django.utils.translation import gettext_lazy as _

from items.models import ItemBase


class UserManager(BaseUserManager):
    def create_user(self, username="", email="", password="", **extra_fields):
        if not email:
            raise ValueError("Enter an email address")
        email = self.normalize_email(email)
        user = self.model(username=username, email=email, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, username="", email="", password=""):
        user = self.create_user(email=email, password=password, username=username)
        user.is_superuser = True
        user.is_staff = True
        user.save(using=self._db)
        return user


class User(AbstractUser, ItemBase):
    # change username to non-editable non-required field
    username = models.CharField(
        _("Username"), max_length=150, editable=False, blank=True
    )
    # change email to unique and required field
    email = models.EmailField(_("Email address"), unique=True)
    bio = models.TextField(_("Bio"))

    USERNAME_FIELD = "email"
    REQUIRED_FIELDS = []

    objects = UserManager()

```

## Creating new items

I will use the Django shell to create several autonomous model instances and the related Items too:

```python
>>> from content.models import Post
>>> from companies.models import Company
>>> from accounts.models import User
>>> from items.models import Item
>>> post = Post.objects.create(
...     title="Hello, World!",
...     slug="hello-world",
...     content="Lorem ipsum…",
... )
>>> company = Company.objects.create(
...     name="Aidas & Co",
...     slug="aidas-co",
...     description="Lorem ipsum…",
... )
>>> user = User.objects.create_user(
...     username="aidas",
...     email="aidas@example.com",
...     password="jdf234oha&6sfhasdfh",
... )
>>> Item.objects.count()
3
```

## Aggregating content from all those relations

Lastly, here is an example of having posts, companies, and users in a single view. For that, we will use the `Item` queryset with annotations:

```python
from django import forms
from django.db import models
from django.shortcuts import render
from django.utils.translation import gettext, gettext_lazy as _

from .models import Item


class SearchForm(forms.Form):
    q = forms.CharField(label=_("Search"), required=False)
    

def all_items(request):
    qs = Item.objects.annotate(
        title=models.Case(
            models.When(
                item_type="content.Post", 
                then="content_post__title",
            ),
            models.When(
                item_type="companies.Company", 
                then="companies_company__name",
            ),
            models.When(
                item_type="accounts.User",
                then="accounts_user__email",
            ),
            default=models.Value(gettext("<Untitled>")),
        ),
        description=models.Case(
            models.When(
                item_type="content.Post",
                then="content_post__content",
            ),
            models.When(
                item_type="companies.Company",
                then="companies_company__description",
            ),
            models.When(
                item_type="accounts.User", 
                then="accounts_user__bio",
            ),
            default=models.Value(""),
        ),
    )
    
    form = SearchForm(data=request.GET, prefix="search")
    if form.is_valid():
        query = form.cleaned_data["q"]
        if query:
            qs = qs.annotate(
                search=SearchVector(
                    "title",
                    "description",
                )
            ).filter(search=query)

    context = {
        "queryset": qs,
        "search_form": form,
    }
    return render(request, "items/all_items.html", context)

```

## Final words

You can have generic functionality and still avoid multiple hits to the database by using the `Item` one-to-one approach instead of generic relations.

The name of the `Item` model can be different, and you can even have multiple such models for various purposes, for example, `TaggedItem` for tags only.

Do you use anything similar in your projects?

Do you see how this approach could be improved?

Let me know in the comments!

---

Cover picture by [Pixabay](https://www.pexels.com/photo/assemble-challenge-combine-creativity-269399/)