如何使用 GenericRelation 的逆
Posted
技术标签:
【中文标题】如何使用 GenericRelation 的逆【英文标题】:How to use inverse of a GenericRelation 【发布时间】:2016-07-09 20:58:21 【问题描述】:我一定是对 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')
使用干净的数据库、同步表并创建一些实例:
>>> 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 文档通常都很棒,但我不明白为什么第二个查询或找到任何关于该行为的文档 - 它似乎完全从错误的表中返回数据?
【问题讨论】:
注意: Django 版本是(1, 7, 11, 'final', 0)
。我无法在 Django 1.8 中重现这一点。
这是否意味着他们决定在 Django 1.7 中修复 1.8 版本?
可能,但我搜索了发行说明中提到的内容,但找不到。我想git bisect
可以找到它....
是的,发行说明很少详细说明发生的一切。查看所有变化的唯一真正方法是查看历史......
由于这是一个特定于版本的错误,您不应该在这个恕我直言上浪费一分钟并尽快升级。恕我直言,即使是 1.8 也是旧版本。
【参考方案1】:
评论 - 为时已晚 - 大多数已删除
问题 #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:
设置
related_query_name
创建从相关对象到此对象的关系。这允许从相关对象中查询和过滤。
如果可以使用特定的关系名称而不仅仅是泛型,那就太好了。使用文档中的示例:taged_item.bookmarks
比 taged_item.content_object
更具可读性,但实现它并不值得。
【讨论】:
【参考方案2】:TL;DR 这是 Django 1.7 中的一个错误,已在 Django 1.8 中修复。
修复提交:1c5cbf5e5d5b350f4df4aca6431d46c767d3785a 修复公关:GenericRelation filtering targets related model's pk 漏洞票:Should filter on related model primary key value, not the object_id value更改直接转到 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.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)
脚本必须是可执行的,所以添加带有chmod +x test_script.py
的标志。它应该位于 Django 被克隆到的目录中,即 /tmp/django/test_script.py
对我来说。这是因为import django
应该首先选择本地签出的 django 项目,而不是站点包中的任何版本。
git bisect 的用户界面旨在找出错误出现的位置,因此当您试图找出某个特定错误发生的时间时,“坏”和“好”的通常前缀是向后的错误已修复。这可能看起来有点颠倒,但如果错误存在,测试脚本应该成功退出(返回代码 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
因此,此过程将执行自动搜索,最终找到修复错误的提交。这需要一些时间,因为 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 更改的提交(从错误的表中获取数据)
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 一分为二可能很难设置!
【讨论】:
以上是关于如何使用 GenericRelation 的逆的主要内容,如果未能解决你的问题,请参考以下文章
在迁移中处理 GenericRelation 和 GenricForeignKey