Django: добавление "NULLS LAST" к запросу

Я хотел бы отсортировать модель, используя параметр Postgresql "NULLS LAST". Как это можно сделать?

Я пробовал что-то вроде

MyModel.objects.all().extra(order_by=('-price', 'NULLS LAST'))

Но я получаю

"Не удается разрешить ключевое слово" NULLS LAST "в поле"

Ответ 1

Ближайшая вещь, которую я нашел, делает это на двух шагах. Первый порядок по заполненному полю, а затем по нулям:

Через этот gist (сам через эти журналы django):

all_projects = Project.objects.select_related().filter(
    company=company).order_by('-date_due')

q = all_projects.extra(select={'date_due_null': 'date_due is null'})
q = q.extra(order_by=['date_due_null'])
print q.query

Удачи.

Ответ 2

from django.db.models import F  
MyModel.objects.all().order_by(F('price').desc(nulls_last=True))

Эта функциональность была добавлена в Django 1.11.

https://docs.djangoproject.com/en/dev/releases/1.11/

Добавлены параметры nulls_first и nulls_last в Expression.asc() и desc() для управления порядком нулевых значений.

Ответ 3

Если вы хотите, чтобы это было сделано прозрачно и на всех столбцах, вы можете переопределить генерацию sql. Для этого вам нужно будет иметь собственный менеджер для возврата пользовательского QuerySet, чтобы вернуть пользовательский запрос для использования пользовательского компилятора. Мой код для этого выглядит так (Django 1.5):

from django.db import models, connections

class NullsLastQuery(models.sql.query.Query):
    """
    Query that uses custom compiler,
    to utilize PostgreSQL feature of setting position of NULL records
    """
    def get_compiler(self, using=None, connection=None):
        if using is None and connection is None:
            raise ValueError("Need either using or connection")
        if using:
            connection = connections[using]

        # defining that class elsewhere results in import errors
        from django.db.models.sql.compiler import SQLCompiler
        class NullsLastSQLCompiler(SQLCompiler):
            def get_ordering(self):
                result, group_by = super(NullsLastSQLCompiler, self
                    ).get_ordering()
                if self.connection.vendor == 'postgresql' and result:
                    result = [line + " NULLS LAST" for line in result]
                return result, group_by

        return NullsLastSQLCompiler(self, connection, using)

class NullsLastQuerySet(models.query.QuerySet):
    def __init__(self, model=None, query=None, using=None):
        super(NullsLastQuerySet, self).__init__(model, query, using)
        self.query = query or NullsLastQuery(self.model)

class NullsLastManager(models.Manager):
    def get_query_set(self):
        return NullsLastQuerySet(self.model, using=self._db)

class YourModel(models.Model):
    objects = NullsLastManager()

Ответ 4

Возможно, этого не было, когда задавался вопрос, но, начиная с Django 1.8, я думаю, что это лучшее решение:

from django.db.models import Coalesce, Value
MyModel.objects.all().annotate(price_null=
    Coalesce('price', Value(-100000000)).order_by('-price_null')

Coalesce выбирает первое ненулевое значение, поэтому вы создаете значение price_null для заказа, по которому указывается просто цена, но с заменой null на -100000000 (или +?).

Ответ 5

Для Django 1.9 (и, возможно, 1.8) вы можете использовать это:

from django.db import connections, models
from django.db.models.sql.compiler import SQLCompiler


class NullsLastSQLCompiler(SQLCompiler):
    def get_order_by(self):
        result = super().get_order_by()
        if result and self.connection.vendor == 'postgresql':
            return [(expr, (sql + ' NULLS LAST', params, is_ref))
                    for (expr, (sql, params, is_ref)) in result]
        return result


class NullsLastQuery(models.sql.query.Query):
    """Use a custom compiler to inject 'NULLS LAST' (for PostgreSQL)."""

    def get_compiler(self, using=None, connection=None):
        if using is None and connection is None:
            raise ValueError("Need either using or connection")
        if using:
            connection = connections[using]
        return NullsLastSQLCompiler(self, connection, using)


class NullsLastQuerySet(models.QuerySet):
    def __init__(self, model=None, query=None, using=None, hints=None):
        super().__init__(model, query, using, hints)
        self.query = query or NullsLastQuery(self.model)

И затем на вашей модели (-ях):

objects = NullsLastQuerySet.as_manager()

Это основано на ответе Тима в fooobar.com/questions/31281/....

Билет для добавления поддержки для этого в Django был повторно открыт: https://code.djangoproject.com/ticket/13312.

Ответ 6

@kabucey answer лучше всего подходит для Django> = 1.11, но если вы используете хотя бы Django 1.8, 1.9 или 1.10, вы можете использовать собственное выражение Func для достижения поведения "NULLS Last", как описано в https://www.isotoma.com/blog/2015/11/23/sorting-querysets-with-nulls-in-django/:

from django.db.models import Func

class IsNull(Func):
    template = '%(expressions)s IS NULL'

MyModel.objects.all().annotate(
    price_isnull=IsNull('price_isnull'),
    ).order_by(
        'price_isnull',
        '-price',
        )

Первый аргумент order_by сортирует список в возрастающем порядке по price_isnull, заставляя элементы с нулевой ценой заканчивать список с True > False.

Ответ 7

Есть еще один способ добавить функциональность управляемых нулей в Django & lt; v1.11 со стилем Django v1.11:

from my_project.utils.django import F
MyModel.objects.all().order_by(F('price').desc(nulls_last=True))
# or
MyModel.objects.all().order_by(F('price').desc().nullslast())

Минусы:

  1. Легкая миграция в Django 1.11
  2. Мы не углубляемся во внутренности компилятора запросов

Для этого нам нужно переопределить классы django.db.models.F и django.db.models.expressions.OrderBy:

from django.db.models import F as DjangoF
from django.db.models.expression import OrderBy as DjangoOrderBy


class OrderBy(DjangoOrderBy):
    def __init__(self, expression, descending=False, nulls_last=None):
        super(OrderBy, self).__init__(expression, descending)
        self.nulls_last = nulls_last
    ...

    def as_sql(self, compiler, connection, template=None, **extra_context):
        ...
        ordering_value = 'DESC' if self.descending else 'ASC'
        if self.nulls_last is not None:
            nulls_value = 'LAST' if self.nulls_last else 'FIRST'
            ordering_value += ' NULLS ' + nulls_value

        placeholders = {
            'expression': expression_sql,
            'ordering': ordering_value,
        }
        ...

    def nullslast(self):
        self.nulls_last = True

    def nullsfirst(self):
        self.nulls_last = False


class F(DjangoF):
    ...

    def asc(self, nulls_last=None):
        return OrderBy(self, nulls_last=nulls_last)

    def desc(self, nulls_last=None):
        return OrderBy(self, descending=True, nulls_last=nulls_last)