Подтвердить что ты не робот

Как использовать инверсию GenericRelation

Я действительно должен неправильно понимать что-то с полем GenericRelation из структуры типов контента Django.

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

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

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

Почему обратное отношение снова возвращает id из 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))

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

4b9b3361

Ответ 1

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

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

создайте следующий файл:

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

script должен быть исполняемым, поэтому добавьте флаг chmod +x test_script.py. Он должен быть расположен в каталоге, в который Django клонирован, т.е. /tmp/django/test_script.py для меня. Это связано с тем, что import django сначала берет локальный проект django, а не любую версию из пакетов сайта.

Пользовательский интерфейс git bisect был разработан, чтобы выяснить, где появились ошибки, поэтому обычные префиксы "плохого" и "хорошего" обращаются назад, когда вы пытаетесь выяснить, когда исправлена ​​определенная ошибка. Это может показаться несколько перевернутым, но тест script должен выйти с успехом (код возврата 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 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 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) )

Конечно, из хэша commit мы можем легко найти запрос на вытягивание и билет на github. Надеюсь, это может помочь кому-то еще один день - дезактивация Django может быть сложной настройкой из-за миграции!

Ответ 2

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

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

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

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

docs: Обратные общие отношения:

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

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