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:
- D = A - B
- 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"]
精彩评论