如何使用 GenericRelation 的逆函数

How to use inverse of a GenericRelation

我一定是对 Django 的内容类型框架中的 GenericRelation field 产生了误解。

为了创建一个最小的独立示例,我将使用教程中的投票示例应用程序。在 Choice 模型中添加一个通用外键字段,并创建一个新的 Thing 模型:

class Choice(models.Model):
    ...
    content_type = models.ForeignKey(ContentType)
    object_id = models.PositiveIntegerField()
    thing = GenericForeignKey('content_type', 'object_id')

class Thing(models.Model):
    choices = GenericRelation(Choice, related_query_name='things')

使用干净的数据库,同步 tables,并创建几个实例:

>>> poll = Poll.objects.create(question='the question', pk=123)
>>> thing = Thing.objects.create(pk=456)
>>> choice = Choice.objects.create(choice_text='the choice', pk=789, poll=poll, thing=thing)
>>> choice.thing.pk
456
>>> thing.choices.get().pk
789

到目前为止一切顺利 - 这种关系在一个实例的两个方向上都有效。但是从查询集来看,反向关系很奇怪:

>>> Choice.objects.values_list('things', flat=1)
[456]
>>> Thing.objects.values_list('choices', flat=1)
[456]

为什么逆向关系又给我来自thing的id?我期望的是选择的主键,相当于以下结果:

>>> Thing.objects.values_list('choices__pk', flat=1)
[789]

那些 ORM 查询生成 SQL 如下:

>>> print Thing.objects.values_list('choices__pk', flat=1).query
SELECT "polls_choice"."id" FROM "polls_thing" LEFT OUTER JOIN "polls_choice" ON ( "polls_thing"."id" = "polls_choice"."object_id" AND ("polls_choice"."content_type_id" = 10))
>>> print Thing.objects.values_list('choices', flat=1).query
SELECT "polls_choice"."object_id" FROM "polls_thing" LEFT OUTER JOIN "polls_choice" ON ( "polls_thing"."id" = "polls_choice"."object_id" AND ("polls_choice"."content_type_id" = 10))

Django 文档通常都很棒,但我不明白为什么第二次查询或找到任何关于该行为的文档 - 似乎 return 数据完全错误 table?

TL;DR 这是 Django 1.7 中的错误,已在 Django 1.8 中修复。

更改直接进入 master 并且没有进入弃用期,这并不奇怪,因为在这里保持向后兼容性真的很困难。更令人惊讶的是,1.8 release notes 中没有提及该问题,因为修复更改了当前工作代码的行为。

这个答案的其余部分描述了我如何使用 git bisect run 找到提交。它在这里仅供我自己参考,所以如果我需要再次平分一个大项目,我可以回到这里。


首先我们设置了一个 django 克隆和一个测试项目来重现这个问题。我在这里使用了 virtualenvwrapper ,但是您可以根据需要进行隔离。

cd /tmp
git clone https://github.com/django/django.git
cd django
git checkout tags/1.7
mkvirtualenv djbisect
export PYTHONPATH=/tmp/django  # get django clone into sys.path
python ./django/bin/django-admin.py startproject djbisect
export PYTHONPATH=$PYTHONPATH:/tmp/django/djbisect  # test project into sys.path
export DJANGO_SETTINGS_MODULE=djbisect.mysettings

创建以下文件:

# /tmp/django/djbisect/djbisect/models.py
from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation

class GFKmodel(models.Model):
    content_type = models.ForeignKey(ContentType)
    object_id = models.PositiveIntegerField()
    gfk = GenericForeignKey()

class GRmodel(models.Model):
    related_gfk = GenericRelation(GFKmodel)

还有这个:

# /tmp/django/djbisect/djbisect/mysettings.py
from djbisect.settings import *
INSTALLED_APPS += ('djbisect',)

现在我们有了一个工作项目,创建 test_script.pygit bisect run:

一起使用
#!/usr/bin/env python
import subprocess, os, sys

db_fname = '/tmp/django/djbisect/db.sqlite3'
if os.path.exists(db_fname):
    os.unlink(db_fname)

cmd = 'python /tmp/django/djbisect/manage.py migrate --noinput'
subprocess.check_call(cmd.split())

import django
django.setup()

from django.contrib.contenttypes.models import ContentType
from djbisect.models import GFKmodel, GRmodel

ct = ContentType.objects.get_for_model(GRmodel)
y = GRmodel.objects.create(pk=456)
x = GFKmodel.objects.create(pk=789, content_type=ct, object_id=y.pk)

query1 = GRmodel.objects.values_list('related_gfk', flat=1)
query2 = GRmodel.objects.values_list('related_gfk__pk', flat=1)

print(query1)
print(query2)

print(query1.query)
print(query2.query)

if query1[0] == 789 == query2[0]:
    print('FIXED')
    sys.exit(1)
else:
    print('UNFIXED')
    sys.exit(0)

脚本必须是executable,所以用chmod +x test_script.py添加flag。它应该位于 Django 被克隆到的目录中,即 /tmp/django/test_script.py 对我来说。这是因为 import django 应该首先选择本地签出的 django 项目,而不是站点包中的任何版本。

gitbisect 的用户界面旨在找出错误出现的位置,因此 "bad" 和 "good" 的常用前缀当您试图找出某个错误何时被 修复 时,它们是倒退的。这可能看起来有些颠倒,但如果存在错误,测试脚本应该成功退出(return 代码 0),如果错误存在,它应该失败(使用非零 return 代码)固定的。这让我绊倒了几次!

git bisect start --term-new=fixed --term-old=unfixed
git bisect fixed tags/1.8
git bisect unfixed tags/1.7
git bisect run ./test_script.py

所以这个过程将进行自动搜索,最终找到修复错误的提交。这需要一些时间,因为在 Django 1.7 和 Django 1.8 之间有很多提交。它平分了 1362 个修改,大约 10 个步骤,最终输出:

1c5cbf5e5d5b350f4df4aca6431d46c767d3785a is the first fixed commit
commit 1c5cbf5e5d5b350f4df4aca6431d46c767d3785a
Author: Anssi Kääriäinen <akaariai@gmail.com>
Date:   Wed Dec 17 09:47:58 2014 +0200

    Fixed #24002 -- GenericRelation filtering targets related model's pk

    Previously Publisher.objects.filter(book=val) would target
    book.object_id if book is a GenericRelation. This is inconsistent to
    filtering over reverse foreign key relations, where the target is the
    related model's primary key.

这正是查询从不正确的 SQL 更改的提交(从错误的 table 获取数据)

SELECT "djbisect_gfkmodel"."object_id" FROM "djbisect_grmodel" LEFT OUTER JOIN "djbisect_gfkmodel" ON ( "djbisect_grmodel"."id" = "djbisect_gfkmodel"."object_id" AND ("djbisect_gfkmodel"."content_type_id" = 8) )

进入正确的版本:

SELECT "djbisect_gfkmodel"."id" FROM "djbisect_grmodel" LEFT OUTER JOIN "djbisect_gfkmodel" ON ( "djbisect_grmodel"."id" = "djbisect_gfkmodel"."object_id" AND ("djbisect_gfkmodel"."content_type_id" = 8) )

当然,从提交哈希我们可以在 github 上轻松找到拉取请求和票证。希望有一天这也能帮助其他人 - 由于迁移,平分 Django 可能很难设置!

评论 - 来不及回答 - 大多数已删除

向后不兼容修复问题 #24002 的一个不重要的结果是 GenericRelatedObjectManager(例如 things)长时间停止查询集的工作并且它只能用于过滤器等.

>>> choice.things.all()
TypeError: unhashable type: 'GenericRelatedObjectManager'
# originally before 1c5cbf5e5:  [<Thing: Thing object>]

半年后 #24940 在 1.8.3 版本和 master 分支中修复了这个问题。这个问题并不重要,因为通用名称 thing 在没有查询的情况下更容易工作 (choice.thing),并且不清楚这种用法是记录还是未记录。

文档:Reverse generic relations

Setting related_query_name creates a relation from the related object back to this one. This allows querying and filtering from the related object.

如果可以使用特定关系名称而不是仅使用通用名称,那就太好了。使用文档中的示例:taged_item.bookmarkstaged_item.content_object 更具可读性,但实现它不值得。