如何使用GenericRelation的逆

wim*_*wim 12 python sql django generic-foreign-key django-generic-relations

我必须真正误解Django的内容类型框架中的GenericRelation字段.

要创建一个最小的自包含示例,我将使用教程中的民意调查示例应用程序.将通用外键字段添加到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')
Run Code Online (Sandbox Code Playgroud)

使用干净的数据库,同步表,并创建一些实例:

>>> 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
Run Code Online (Sandbox Code Playgroud)

到目前为止一切都很好 - 关系在一个实例的两个方向上都有效.但是从查询集中,反向关系非常奇怪:

>>> Choice.objects.values_list('things', flat=1)
[456]
>>> Thing.objects.values_list('choices', flat=1)
[456]
Run Code Online (Sandbox Code Playgroud)

为什么反向关系再次给我一个id thing?我期待相当于选择的主键,相当于以下结果:

>>> Thing.objects.values_list('choices__pk', flat=1)
[789]
Run Code Online (Sandbox Code Playgroud)

那些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))
Run Code Online (Sandbox Code Playgroud)

Django文档通常很好,但我无法理解为什么第二个查询或找到该行为的任何文档 - 它似乎完全从错误的表返回数据?

wim*_*wim 7

TL; DR这是在Django 1.8中修复的Django 1.7中的一个错误.

这个变化直接转变为主人并没有进入弃用期,这并不太令人惊讶,因为在这里保持向后兼容性确实很困难.更令人惊讶的是,1.8发行说明中没有提到该问题,因为该修补程序改变了当前工作代码的行为.

这个答案的其余部分是我如何使用发现提交的描述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
Run Code Online (Sandbox Code Playgroud)

创建以下文件:

# /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)
Run Code Online (Sandbox Code Playgroud)

还有这个:

# /tmp/django/djbisect/djbisect/mysettings.py
from djbisect.settings import *
INSTALLED_APPS += ('djbisect',)
Run Code Online (Sandbox Code Playgroud)

现在我们有一个工作项目,创建test_script.py使用git 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)
Run Code Online (Sandbox Code Playgroud)

该脚本必须是可执行的,因此请添加标志chmod +x test_script.py.它应该位于Django克隆到的目录中,即/tmp/django/test_script.py对我来说.这是因为首先import django应该选择本地签出的django项目,而不是来自site-packages的任何版本.

混帐对开的用户界面设计,找出错误出现,这样的"坏"和"好"的常用前缀是向后当你试图找出当某个错误是固定的.这可能看起来有些颠倒,但如果错误存在,测试脚本应该成功退出(返回代码0),如果修复了错误,它应该失败(使用非零返回代码).这让我绊了几次!

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
Run Code Online (Sandbox Code Playgroud)

因此,此过程将执行自动搜索,最终找到修复错误的提交.这需要一些时间,因为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.
Run Code Online (Sandbox Code Playgroud)

这恰恰是查询从不正确的SQL(从错误的表中获取数据)发生更改的提交

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) )
Run Code Online (Sandbox Code Playgroud)

进入正确的版本:

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) )
Run Code Online (Sandbox Code Playgroud)

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