Объединенный столбец order_by в Джанго

У меня есть две модели, которые наследуются от другой модели. Пример:

class Parent(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, verbose_name = "ID")


class A(Parent):
    name = models.CharField(max_length=255, verbose_name = "Name")


class BProxy(Parent):
    target = models.OneToOneField('B', on_delete=models.CASCADE)


class B(models.Model):
    name = models.CharField(max_length=255, verbose_name = "Name")

Мой запрос на данный момент выглядит так:

Parent.objects.all()

В своем сериализаторе я проверяю, к какому подклассу относится родительский объект (hasattr(obj, 'a')), а затем использую либо name = obj.a.name, либо name = obj.b.target.name для сериализованных данных.

Но теперь я хотел бы отсортировать набор запросов для вывода. Обычно я бы использовал здесь Parent.objects.all().order_by('name'). Но имя находится в подклассах.

Можно ли объединить столбцы «имени» двух подклассов и затем выполнить сортировку по ним? Или есть другое решение?

🤔 А знаете ли вы, что...
Основной принцип Django — DRY (Don't Repeat Yourself), что способствует уменьшению дублирования кода.


50
3

Ответы:

order_by может принимать имя поля в подклассах, т. е. Parent.objects.all().order_by('-bproxy__target__name', '-a__name'). Это даст следующий запрос, который будет упорядочен на основе имени A и имени B.

SELECT "tmp_parent"."id" FROM "tmp_parent" LEFT OUTER JOIN "tmp_bproxy" ON ("tmp_parent"."id" = "tmp_bproxy"."parent_ptr_id") LEFT OUTER JOIN "tmp_b" ON ("tmp_bproxy"."target_id" = "tmp_b"."id") LEFT OUTER JOIN "tmp_a" ON ("tmp_parent"."id" = "tmp_a"."parent_ptr_id") ORDER BY "tmp_b"."name" DESC, "tmp_a"."name" DESC

сначала будет заказан A, а затем B, если вы хотите сделать заказ как для A, так и для B, рассмотрите ответ @temunel или используйте

    from django.db.models.functions import Coalesce

    Parent.objects.annotate(
        name=Coalesce("a__name", "bproxy__target__name")
    ).order_by('name')

это означает, что если a__name имеет значение null или не существует, будет использоваться значение bproxy__target__name.

Примечание: не все базы данных имеют COALESCE(), а значения a__name и bproxy__target__name должны быть совместимого типа для упорядочивания.


Решено

Чтобы отсортировать объекты Parent на основе поля name их подклассов, вам необходимо выполнить annotating набор запросов с полем name из обоих подклассов, а затем упорядочить их по этому аннотированному полю.

Вот как вы можете это сделать:

from django.db.models import Case, When, Value, CharField

queryset = Parent.objects.annotate(
    name=Case(
        When(a__isnull=False, then='a__name'),
        When(bproxy__isnull=False, then='bproxy__target__name'),
        default=Value(''),
        output_field=CharField(),
    )
).order_by('name')

Я надеюсь, что это решит вашу проблему.


Для меня это звучит почти как проблема XY, но трудно сказать, не имея дополнительной информации о том, чего вы пытаетесь достичь. Например, вы спрашиваете о том, как сделать X, но вам действительно нужен ответ, как лучше сделать Y. Однако я отвечу, предполагая, что вам нужно сделать X.

Судя по вашему запросу Parent.objects.all(), вы не сможете делать заказ по имени, поскольку вы запрашиваете только таблицу Parent. Вам все равно придется искать нужные данные в A или BProxy (что затем позволит вам выполнить каскадный поиск B). Мой вопрос заключается в том, действительно ли Parent должна быть отдельной таблицей или вам следует использовать абстрактную таблицу и/или прокси-таблицу. Я не могу советовать вам это, не имея дополнительных знаний о том, что вы пытаетесь сделать, но вы можете прочитать некоторые статьи об этом.

Одним из подходов, помогающих преобразовать Parent в его A или BProxy дочерние элементы, было бы создание такой вспомогательной функции:

class Parent(models.Model):
    ...
    def child(self):
        try:
            return A.objects.get(id=self.id)
        except A.DoesNotExist:
            return BProxy.objects.get(id=self.id)

Как написано, он не идеален, поскольку выдает сбивающую с толку ошибку BProxy.DoesNotExist, если строка Parent не соответствует ничему ни в A, ни в BProxy. Однако было бы нормально, если бы все Parent всегда были одним или другим. Теперь вы можете преобразовать QuerySet в список дочерних объектов:

children = [x.child() for x in m.Parent.objects.all()]

Это означает, что все запрошенные строки будут получены в это время, и вам придется отсортировать их в Python, но на данный момент это относительно просто. Чтобы сделать BProxy более похожим на A, вы можете определить для него свойство следующим образом:

class BProxy(Parent):
    ...
    @property
    def name(self):
        return self.target.name

Теперь вы можете использовать sorted() для свойства name, которое есть у них обоих:

    ordered_children = sorted(children, key=lambda x: x.name)