django - ordering queryset by a calculated field

后端 未结 6 679
故里飘歌
故里飘歌 2020-11-30 04:22

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


        
相关标签:
6条回答
  • 2020-11-30 04:36

    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'])
    
    0 讨论(0)
  • 2020-11-30 04:37

    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()
    
    0 讨论(0)
  • 2020-11-30 04:40

    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'))
    
    0 讨论(0)
  • 2020-11-30 04:47

    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.

    0 讨论(0)
  • 2020-11-30 04:50

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

    0 讨论(0)
  • 2020-11-30 04:52

    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 讨论(0)
提交回复
热议问题