Django 查询按多对多关系最新版本的字段排序
Django query to sort by a field on the latest version of a Many to Many relationship
假设我有以下 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}"
我想编写一个查询来获取所有工具并 returns 它们按最新工具箱的名称排序。我知道我可以使用以下代码实现此目的:
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
table 有数千条记录,这需要几分钟才能执行。我可以编写更快的查询吗?
我尝试了以下方法,但它返回了重复项并且没有正确排序工具:
Tool.objects.order_by("toolboxes__name")
# <QuerySet [<Tool: Tool 1>, <Tool: Tool 2>, <Tool: Tool 1>]>
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()
不确定这些中的任何一个是否可以开箱即用,但沿着这些路线可以工作,基本上不是查询每个工具工具箱,而是预取所有关系,然后过滤它们,从而改为执行 1 或 2 个查询许多
使用 Subquery
and 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
,其中将包含其关联工具箱的最新版本名称。
假设我有以下 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}"
我想编写一个查询来获取所有工具并 returns 它们按最新工具箱的名称排序。我知道我可以使用以下代码实现此目的:
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
table 有数千条记录,这需要几分钟才能执行。我可以编写更快的查询吗?
我尝试了以下方法,但它返回了重复项并且没有正确排序工具:
Tool.objects.order_by("toolboxes__name")
# <QuerySet [<Tool: Tool 1>, <Tool: Tool 2>, <Tool: Tool 1>]>
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()
不确定这些中的任何一个是否可以开箱即用,但沿着这些路线可以工作,基本上不是查询每个工具工具箱,而是预取所有关系,然后过滤它们,从而改为执行 1 或 2 个查询许多
使用 Subquery
and 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
,其中将包含其关联工具箱的最新版本名称。