如何在Django中保留额外条件的外部联接

时间:2018-07-04 13:40:29

标签: django django-models

我有以下三种模型:

class Track(models.Model):
    title = models.TextField()
    artist = models.TextField()

class Tag(models.Model):
    name = models.CharField(max_length=50)

class TrackHasTag(models.Model):
    track = models.ForeignKey('Track', on_delete=models.CASCADE)
    tag = models.ForeignKey('Tag', on_delete=models.PROTECT)

我想检索所有未用特定标签标记的轨道。这可以得到我想要的东西:Track.objects.exclude(trackhastag__tag_id='1').only('id'),但是当表增长时,它非常慢。这是在打印查询集的.query时得到的:

SELECT "track"."id" 
FROM   "track" 
WHERE  NOT ( "track"."id" IN (SELECT U1."track_id" AS Col1 
                              FROM   "trackhastag" U1 
                              WHERE  U1."tag_id" = 1) )

我希望Django发送此查询:

SELECT "track"."id" 
FROM   "track" 
       LEFT OUTER JOIN "trackhastag" 
                    ON "track"."id" = "trackhastag"."track_id" 
                       AND "trackhastag"."tag_id" = 1 
WHERE  "trackhastag"."id" IS NULL; 

但是还没有找到一种方法。使用原始查询并不是真正的选择,因为我必须经常过滤结果查询集。

我发现最干净的解决方法是在数据库中创建视图,并使用TrackHasTagFoo和模型managed = False创建模型,Track.objects.filter(trackhastagfoo__isnull=True)用于查询。我认为这不是一个优雅也不可持续的解决方案,因为它涉及在我的迁移中添加Raw SQL来维持上述观点。

这只是一个例子,其中我们需要在额外条件下进行这种左连接,但事实是,我们在应用程序的更多部分都面临着这个问题。

非常感谢!

4 个答案:

答案 0 :(得分:9)

Django #29555中所述,您可以为此目的since Django 2.0使用FilteredRelation

Track.objects.annotate(
    has_tag=FilteredRelation(
        'trackhastag', condition=Q(trackhastag__tag=1)
    ),
).filter(
    has_tag__isnull=True,
)

答案 1 :(得分:0)

使用过滤器比排除方法要好...因为排除才可以首先获取整个查询,而且比排除不想要的itens更好,而过滤器只获取您想要的内容,就像您所说的 Track.objects.filter (trackhastagfoo__isnull = True)比排除一个更好。

建议:正如穆罕默德所说,您试图手动建立一个ManyToMany关系,为什么不尝试使用ManyToManyField?更容易使用

也许这可以回答您的问题:Django Left Outer Join

答案 2 :(得分:0)

queryset Extras又如何呢?它们不会破坏ORM,并且可以进一步过滤(与RawSQL相比)

from django.db.models import Q

Track.objects.filter(
  # work around to force left outer join
  Q(trackhastag__isnull=True) | Q(trackhastag__isnull=False)
).extra(
    # where parameters are “AND”ed to any other search criteria
    # thus we need to account for NULL
    where=[
        '"app_trackhastag"."id" <> %s or "app_trackhastag"."id" is NULL'
    ],
    params=[1],
)

产生这个有点复杂的查询:

SELECT "app_track"."id", "app_track"."title", "app_track"."artist"
FROM "app_track"
LEFT OUTER JOIN "app_trackhastag"
ON ("app_track"."id" = "app_trackhastag"."track_id")
WHERE (
    ("app_trackhastag"."id" IS NULL OR "app_trackhastag"."id" IS NOT NULL) AND
    ("app_trackhastag"."id" <> 1 or "app_trackhastag"."id" is NULL)
)

理性

步骤1

以下是使用queryset进行左外部联接的一种简单方法:

Track.objects.filter(trackhastag__isnull=True)

给出:

SELECT "app_track"."id", "app_track"."title", "app_track"."artist"
FROM "app_track"
LEFT OUTER JOIN "app_trackhastag"
ON ("app_track"."id" = "app_trackhastag"."track_id")
WHERE "app_trackhastag"."id" IS NULL

步骤2

意识到第一步完成后(我们有一个左外部联接),我们可以利用 queryset's extra

Track.objects.filter(
    trackhastag__isnull=True
).extra(
    where=['"app_trackhastag"."id" <> %s'],
    params=[1],
)

给出:

SELECT "app_track"."id", "app_track"."title", "app_track"."artist"
FROM "app_track"
LEFT OUTER JOIN "app_trackhastag"
ON ("app_track"."id" = "app_trackhastag"."track_id")
WHERE (
    "app_trackhastag"."id" IS NULL AND
    ("app_trackhastag"."id" <> 1)
)

步骤3

解决extra的局限性(将所有参数“与”所有其他搜索条件的所有 )以得出上述最终解决方案。

答案 3 :(得分:-1)

恩瑞克,为什么您没有使用多对多关系

class Track(models.Model):
    title = models.TextField()
    artist = models.TextField()
    tags = models.ManyToManyField(Tag)

class Tag(models.Model):
    name = models.CharField(max_length=50)

还有你的问题

Track.objects.filter(~Q(tags__id=1))