Комбинированный столбец order_by в django

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

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'). Но имя находится в подклассах.

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

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)
Вернуться на верх