【问题标题】:How to left outer join with extra condition in Django如何在 Django 中使用额外条件离开外部连接
【发布时间】:2018-12-12 23:31:09
【问题描述】:

我有这三个模型:

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 添加到我的迁移中以维护所述视图。

这只是我们需要使用额外条件执行这种左连接的情况的一个示例,但事实是我们在应用程序的更多部分都面临这个问题。

非常感谢!

【问题讨论】:

  • Track.objects.exclude(trackhastag__name='new_release').only('id') 对问题中的模型的查询是错误的。
  • @BearBrown 你是对的。现在已更正。对不起!

标签: django django-models


【解决方案1】:

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

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

【讨论】:

    【解决方案2】:

    查询集附加功能怎么样?它们不会破坏 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

    使用查询集进行左外连接的一种直接方式如下:

    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
    

    第二步

    意识到一旦第 1 步完成(我们有一个左外连接),我们就可以利用 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)
    )
    

    第三步

    绕过extra 限制(所有参数都与任何其他搜索条件进行“与”运算)以提出上述最终解决方案。

    【讨论】:

      【解决方案3】:

      使用过滤器比排除更好......因为机智排除他们将首先获得整个查询,并且只排除你不想要的项目,而过滤器只得到你想要的就像你说的那样 Track.objects.filter (trackhastagfoo__isnull=True) 比排除一个要好。

      建议:你试图手动做一个ManyToMany关系,正如Mohammad所说,你为什么不尝试使用ManyToManyField?更容易使用

      也许这回答了你的问题:Django Left Outer Join

      【讨论】:

      • 这并不能真正回答我的问题。我已经回复 Mohammed 说,明确的多对多关系执行与我的示例完全相同的查询。
      【解决方案4】:

      Enric,你为什么不使用多对多关系

      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))
      

      【讨论】:

      • 这会产生与 Django 在我的示例中执行的查询完全相同的查询,但表名除外。
      猜你喜欢
      • 2013-09-11
      • 2021-09-23
      • 2017-07-31
      • 2017-04-27
      • 1970-01-01
      • 2015-03-06
      • 2015-03-12
      • 1970-01-01
      • 2018-04-18
      相关资源
      最近更新 更多