Django запрос для сортировки по полю последней версии отношения "многие ко многим

Допустим, у меня есть следующие модели Django:

class Toolbox(models.Model):
    class Meta:
      constraints = [
          models.UniqueConstraint(
              fields=["name", "version"],
              name="%(app_label)s_%(class)s_unique_name_version",
          )
      ]

    name = models.CharField(max_length=255)
    version = models.PositiveIntegerField()
    tools = models.ManyToManyField("Tool", related_name="toolboxes")

    def __str__(self) -> str:
        return f"{self.name}"

class Tool(models.Model):
    name = models.CharField(max_length=255)

    def __str__(self) -> str:
        return f"{self.name}"

Я хочу написать запрос, который получает все инструменты и возвращает их отсортированными по имени последнего инструмента. Я знаю, что могу достичь этого, используя следующий код:

tools = Tool.objects.all()
for tool in tools:
    tool.latest_toolbox = tool.toolboxes.order_by("-version").first()

tools = sorted(tools, key=lambda x: x.latest_toolbox.name)

Вот модульный тест, написанный на pytest-django, чтобы доказать, что это работает:

from pytest_django.asserts import assertQuerysetEqual

def test_sort_tools_by_latest_toolbox_name():
    tool1 = Tool.objects.create(name="Tool 1")
    tool2 = Tool.objects.create(name="Tool 2")
    toolbox1_v1 = Toolbox.objects.create(name="A", version=1)
    toolbox1_v1.tools.add(tool1)
    toolbox1_v2 = Toolbox.objects.create(name="Z", version=2)
    toolbox1_v2.tools.add(tool1)
    toolbox2_v1 = Toolbox.objects.create(name="B", version=1)
    toolbox2_v1.tools.add(tool2)

    tools = Tool.objects.all()
    for tool in tools:
        tool.latest_toolbox = tool.toolboxes.order_by("-version").first()
    
    tools = sorted(tools, key=lambda x: x.latest_toolbox.name)
    assertQuerysetEqual(tools, [tool2, tool1])

Однако таблица Tool содержит тысячи записей, и на выполнение запроса уходят минуты. Могу ли я написать более быстрый запрос?

Я пробовал следующее, но он возвращает дубликаты и не сортирует инструменты правильно:

Tool.objects.order_by("toolboxes__name")
# <QuerySet [<Tool: Tool 1>, <Tool: Tool 2>, <Tool: Tool 1>]>

Используйте prefetch_related

toolbox_qs = Toolbox.objects.order_by("-version")
tools_qs = Tool.objects.prefetch_related(Prefetch('toolboxes', query=toolbox_qs)
for tool in tools_qs:
    tool.latest_toolbox = tool.toolboxes.first()

или я думаю

tools_qs = Tool.objects.prefetch_related('toolboxes')
for tool in tools_qs:
    tool.latest_toolbox = tool.toolboxes.order_by("-version").first()

Не уверен, что любой из этих вариантов будет работать из коробки, но примерно такие же варианты будут работать, в основном вместо запроса каждого инструмента Tools toolboxes, вы предварительно получаете все отношения, а затем фильтруете их и таким образом делаете 1 или 2 запроса вместо многих

Попробуйте этот подход, используя Subquery и OuterRef:

from django.db.models import OuterRef, Subquery

toolbox_subquery = Toolbox.objects.filter(tools=OuterRef('pk')).order_by('-version')
tools_qs = Tool.objects.order_by(Subquery(toolbox_subquery.values('name')[:1]))

Если вам нужен name последний набор инструментов, кроме как для заказа, вы можете просто поместить его в аннотированное поле:

tools_qs = Tool.objects.annotate(latest_toolbox_name=Subquery(toolbox_subquery.values('name')[:1])).order_by('latest_toolbox_name')

Каждый инструмент будет иметь аннотированное поле latest_toolbox_name, которое будет содержать название связанного с ним набора инструментов с последней версией.

Вернуться на верх