Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

django - ordering queryset by a calculated field

Tags:

django

I want to have a model with calculated fields that I can apply sorting on. For example, let's say that I have the following model:

class Foo(models.Model):
    A = models.IntegerField(..)
    B = models.IntegerField(..)
    C = models.ForeignKey(..)

I want to have a D and an E field that are calculated by the following formulas:

  1. D = A - B
  2. E = A - X (where X is a field of the relevant record of model C)

Implementing this would be trivial if I didn't need to apply sorting; I would just add properties to the model class. However, I need ordering by these fields.

A solution is to fetch all records into memory and do the sorting there, which I conceive a last resort (it will break things regarding pagination).

Is there a way to achieve what I'm trying? Any guidance is appreciated.

EDIT: Denormalization is a no-go. The value of field X changes very frequently and a lot of Foo records are related to one record of model C. An update of X will require thousands of updates of E.

like image 544
shanyu Avatar asked Oct 30 '09 22:10

shanyu


4 Answers

If you would not mind some logic duplicaton, then the following will work:

Foo.objects.extra(select={'d_field': 'A - B'}).extra(order_by=['d_field'])
like image 158
Dmitry Risenberg Avatar answered Nov 15 '22 07:11

Dmitry Risenberg


Please refrain from using extra() as it's meant to be deprecated in the future.

Since Django 1.7 you can use a combination of annotate() and order_by() to achieve this

Foo.objects.annotate(ordering=F('A') - F('B')).order_by('ordering')

There's also ungoing work to allow expressions to be used all over the ORM so the following should work in future versions of Django:

Foo.objects.order_by(F('A') - F('B'))
like image 22
Simon Charette Avatar answered Nov 15 '22 08:11

Simon Charette


I would take a look at the extra method on Queryset and specify the order_by parameter.

like image 27
John Paulett Avatar answered Nov 15 '22 09:11

John Paulett


As Simon says, you can now use expressions in queries, and those values will be calculated in the database. Here are the queries you asked about with the new sorting technique:

Foo.objects.order_by(F('a') - F('b'))
Foo.objects.order_by(F('a') - F('bar__x'))

Here's a complete runnable example that plays with these expressions:

# Tested with Django 1.9.2
import logging
import sys

import django
from django.apps import apps
from django.apps.config import AppConfig
from django.conf import settings
from django.db import connections, models, DEFAULT_DB_ALIAS
from django.db.models import F
from django.db.models.base import ModelBase
from django.db.models.functions import Concat, Value

from mock import patch, PropertyMock, MagicMock

NAME = 'udjango'


def main():

    setup()

    class Bar(models.Model):
        x = models.IntegerField()

    class Foo(models.Model):
        a = models.IntegerField()
        b = models.IntegerField()
        bar = models.ForeignKey(Bar)

    syncdb(Bar)
    syncdb(Foo)

    bar1 = Bar.objects.create(x=1)
    bar5 = Bar.objects.create(x=5)
    Foo.objects.create(a=10, b=3, bar=bar1)
    Foo.objects.create(a=13, b=3, bar=bar5)
    Foo.objects.create(a=15, b=9, bar=bar1)

    print(Foo.objects.annotate(ordering=F('a') - F('b'))
          .order_by('ordering').values_list('a', 'b', 'bar__x', 'ordering'))
    # >>> [(15, 9, 1, 6), (10, 3, 1, 7), (13, 3, 5, 10)]

    print(Foo.objects.annotate(ordering=F('a') - F('bar__x'))
          .order_by('ordering').values_list('a', 'b', 'bar__x', 'ordering'))
    # >>> [(13, 3, 5, 8), (10, 3, 1, 9), (15, 9, 1, 14)]

    print(Foo.objects.order_by(F('a') - F('b')).values_list('a', 'b', 'bar__x'))
    # >>> [(15, 9, 1), (10, 3, 1), (13, 3, 5)]

    print(Foo.objects.order_by(F('a') - F('bar__x')).values_list('a', 'b', 'bar__x'))
    # >>> [(13, 3, 5), (10, 3, 1), (15, 9, 1)]

    logging.info('Done.')


def setup():
    db_file = NAME + '.db'
    with open(db_file, 'w'):
        pass  # wipe the database
    settings.configure(
        DEBUG=True,
        DATABASES={
            DEFAULT_DB_ALIAS: {
                'ENGINE': 'django.db.backends.sqlite3',
                'NAME': db_file}},
        LOGGING={'version': 1,
                 'disable_existing_loggers': False,
                 'formatters': {
                    'debug': {
                        'format': '%(asctime)s[%(levelname)s]'
                                  '%(name)s.%(funcName)s(): %(message)s',
                        'datefmt': '%Y-%m-%d %H:%M:%S'}},
                 'handlers': {
                    'console': {
                        'level': 'DEBUG',
                        'class': 'logging.StreamHandler',
                        'formatter': 'debug'}},
                 'root': {
                    'handlers': ['console'],
                    'level': 'INFO'},
                 'loggers': {
                    "django.db": {"level": "DEBUG"}}})
    app_config = AppConfig(NAME, sys.modules['__main__'])
    apps.populate([app_config])
    django.setup()
    original_new_func = ModelBase.__new__

    # noinspection PyDecorator
    @staticmethod
    def patched_new(cls, name, bases, attrs):
        if 'Meta' not in attrs:
            class Meta:
                app_label = NAME
            attrs['Meta'] = Meta
        return original_new_func(cls, name, bases, attrs)
    ModelBase.__new__ = patched_new


def syncdb(model):
    """ Standard syncdb expects models to be in reliable locations.

    Based on https://github.com/django/django/blob/1.9.3
    /django/core/management/commands/migrate.py#L285
    """
    connection = connections[DEFAULT_DB_ALIAS]
    with connection.schema_editor() as editor:
        editor.create_model(model)

main()
like image 23
Don Kirkby Avatar answered Nov 15 '22 09:11

Don Kirkby