Query Selectors

Django is a great framework but the code tends to get messy as time passes by and the project grows. HackSoft has invented a pattern called “Selectors” which takes care of calling ORM methods everywhere and thinking in (and encapsulating) use-cases instead of using the ORM directly and not DRY.

In addition to this the company WirBauenDigital created a coding style which enforces all Custom QuerySet (CQS) methods to be as atomic as possible. These CQS methods will be (re-)used in any place throughout your system.

Combining these approaches with the Ambient Digital style to separate CQS and managers in a very strict way:

  • CQS methods will always return a QuerySet

  • They NEVER alter the data, just fetch it

Definition

The result of this blend is a pattern we’d like to introduce as “Query Selectors”. These selectors follow some rules:

A query selector…

  • is a method or function which represents a single specific use-case (“fetch all active users who like beer”)

  • has a well-defined in- and output and no “magic” (for example, some functionality provided by its class)

  • can be registered in the model to have a django-esque way of calling it (MyModel.selectors.active_users_liking_beer())

Example

Here you’ll see how a selector class could look like. Note, that is_active() and likes_beverages() are CQS methods defined within UserQuerySet which is registered in the UserManager.

# my_app/selectors/user.py

from ai_django_core.selectors.base import Selector


class UserSelector(Selector):
    def active_users_liking_beer(self):
        """
        Fetches a list of active users who like to drink beer.
        """
        return self.model.objects.is_active().likes_beverages(beverage_list=['beer'])

Here is an example on how to register a selector within a model.

# my_app/models.py

from django.db import models


class User(models.Model):
    ...

    objects = UserManager()
    selectors = UserSelector()

To complete the example, we’ll add the CQS and manager as well:

# my_app/managers/user.py

from django.db.models import manager


class UserQuerySet(manager.QuerySet):
    """
    Custom queryset for the "User" clas
    """

    def is_active(self):
        """
        Get all users who have the active flag set to "True"
        """
        return self.filter(is_active=True)

    def likes_beverages(self, beverage_list: list[str]):
        """
        Gets all users who have set at least one of the given list as their favourite beverage
        """
        return self.filter(beverages__name__in=beverage_list)


class UserManager(manager.Manager):
    def active_users_liking_beer(self):
        """
        Fetches a list of active users who like to drink beer.
        """
        return self.model.objects.is_active().likes_bevarages(beverage_list=['beer'])

Remarks

  • Take care that the current selector class inherits from the Django manager. This is a workaround to inject the current model class into the selector to avoid circular dependency errors and access the model manager/CQS via self.model.*. This means that Django will think you already have a custom manager, and you have to register a real custom manager. If you follow the given pattern, you need one anyway so that shouldn’t be an issue. Nevertheless, if you leave one out, you’ll get a weird error because Django will not create a default one.

Permissions & Visibility

Similar to the manager section in this documentation, you can use a neat pattern for handling basic object visibility with a mixin AbstractUserSpecificSelectorMixin. This mixin provides three methods: visible_for(), editable_for() and deletable_for(). Each method needs to be implemented per selector class like this:

# my_app/selector/mymodel.py

from ai_django_core.selectors.base import Selector
from ai_django_core.selectors.permission import AbstractUserSpecificSelectorMixin

class MyModelSelector(AbstractUserSpecificSelectorMixin, Selector):

    def visible_for(self, user):
        ...

    def editable_for(self, user):
        ...

    def deletable_for(self, user):
        ...