Search code examples
pythondjangodjango-modelscomputed-propertiescomputed-field

Django: queryable computed fields


Is there a "best" way of doing computed fields that can be queried?

For example:

from django.db import models

class Person(models.Model):
    given_name = models.CharField(max_length=30)
    family_name = models.CharField(max_length=30)

    NAME_ORDER_CONVENTION_CHOICES = (
        # "Eastern" name order: family name followed by given name
        ('E', 'Eastern'),
        # "Western" name order: given name followed by family name
        ('W', 'Western')
    )

    name_order_convention = models.CharField(
        length=1, 
        choices=NAME_ORDER_CONVENTION_CHOICES,
    )

    @property
    def full_name(self):
        """
        Return full name, calculated on the basis of given name,
        family name and name order convention.
        """
        template = "{} {}"
        if self.name_order_convention == "W":
            return template.format(self.given_name, self.family_name)
        return template.format(self.family_name, self.given_name)

This gives you the ability to get the full_name for any Person, but if you want to do a database query based on full name, you would need to write a query with implicit knowledge of how this property is computed. This would seem to be something of a violation of DRY, in that there is no longer any central place where you can change where how the computed field is calculated - you have to change the full_name property, and anywhere that makes a query based on implicit knowledge of how full_name works.

The main alternative I can think of is overriding the save() method to update these fields on each update.

from django.db import models

class Person(models.Model):
    given_name = models.CharField(max_length=30)
    family_name = models.CharField(max_length=30)

    NAME_ORDER_CONVENTION_CHOICES = (
        ('E', 'Eastern'),
        ('W', 'Western')
    )

    name_order_convention = models.CharField(
        length=1, 
        choices=NAME_ORDER_CONVENTION_CHOICES,
    )

    # Computed fields
    full_name = models.CharField(max_length=60)

    def _calculate_full_name(self):
        template = "{} {}"
        if self.name_order_convention == "W":
            self.full_name = template.format(self.given_name, self.family_name)
        else:
            self.full_name = template.format(self.family_name, self.given_name)

    def save(self, *args, **kwargs):
        self._calculate_full_name()
        super(Model, self).save(*args, **kwargs)

Is there a common, "best-practice" solution for queryable computed fields that I might be missing?


Solution

  • The answer may lie in django's fairly new query annotations:

    https://docs.djangoproject.com/en/1.11/ref/models/expressions/#using-f-with-annotations

    company = Company.objects.annotate(
        chairs_needed=F('num_employees') - F('num_chairs'))