12

Должно быть, я действительно неправильно понимаю что-то с GenericRelation field из структуры типов контента 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') 

С чистой дб, синхронизируются таблицы, и создать несколько экземпляров:

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

До сих пор так хорошо - отношение работает в обоих направлениях от экземпляра. Но из QuerySet, обратная связь очень странно:

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

Почему обратная связь дает мне снова идентификатор из thing? Я ожидал, что вместо первичного ключа выбора, что эквивалентен следующий результат:

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

Документы Джанго, как правило, отлично, но я не могу понять, почему второй запрос или найти какую-либо документацию по этому поведению - кажется, что он полностью возвращает данные из неправильной таблицы?

+0

* примечание: * версия Django '(1, 7, 11, 'final', 0)'. Я не могу воспроизвести это в Django 1.8. – wim

+0

Может ли это означать, что в Django 1.7 они решили исправить для 1.8? – mgilson

+0

Возможно, но я искал высоко и низко для упоминания в примечаниях к выпуску и не мог найти его. Я полагаю, что 'git bisect' мог найти его ... – wim

ответ

7

TL; DR Это была ошибка в Django 1.7, которая была исправлена ​​в Django 1.8.

Изменения пошли непосредственно в освоении и не гибнуть период устаревания, что не слишком удивительно, учитывая, что поддержание обратной совместимости здесь было бы очень сложно. Более удивительно то, что в 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, а не любую версию с сайта-пакетов.

Пользовательский интерфейс мерзавца 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 <[email protected]> 
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 может быть сложной настройкой из-за миграции!

1

Комментарий - слишком поздно для ответа - наиболее удаленные

А, не важно, результат обратного несовместимой затруднительного выпуска #24002 является то, что GenericRelatedObjectManager (например things) перестал работать для запроса набора долгое время, и он может быть использован только для фильтров и т.д.

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

Это было зафиксировано полтора года спустя #24940 в версии 1.8.3 и в главной ветви. Проблема не была важна, поскольку общее имя thing работает проще без запроса (choice.thing), и неясно, что это использование документировано или недокументировано.

документы: Reverse generic relations:

Установка related_query_name создает связь от связанного объекта обратно к этому.Это позволяет запрашивать и фильтровать из связанного объекта.

Было бы здорово, если бы вместо описания было использовано только определенное имя отношения. В примере из документов: taged_item.bookmarks более читабельна, чем taged_item.content_object, но было бы нецелесообразно выполнять ее.

 Смежные вопросы

  • Нет связанных вопросов^_^