开发者

django - ordering queryset by a calculated field

I want to have a model wi开发者_如何学编程th 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.


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'])


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'))


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


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()


I haven't presently got a Django install running, but I think what you're asking is how to do a custom save, such that D and E are automatically generated. I don't know what your ForeignKey's return on unicode is, so I'm assuming it's not a string and assigning "valueName" as token vlaue for the integer you want to usage.

Anyway, it should go a bit like this:

class Foo(models.Model):
    A = models.IntegerField(..)
    B = models.IntegerField(..)
    C = models.ForeignKey(..)
    D = models.IntegerField(..)
    E = models.IntegerField(..)
    def save(self):
        self.D = self.A - self.B
        self.E = self.A - self.C.valueName
        super(Foo, self).save()

Anything prior to the last line of that (super()) will be PRE save, anything after is POST. That's really the most important point there.


I find that without the *args and **kwargs in the save method, it returns an error. And as celopes stated, this is only a solution if you don't mind materializing the computed field in the database.

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

    def save(self, *args, **kwargs):
        self.D = self.A - self.B
        self.E = self.A - self.C.X
        super(Foo, self).save(*args, **kwargs)

    class Meta:
        ordering = ["E", "D"]
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜