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:
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()
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With