12

zu verwenden Ich muss wirklich etwas mit dem GenericRelation field von Django Content-Typen Framework missverstehen.Wie inverse einer GenericRelation

Um ein minimales eigenständiges Beispiel zu erstellen, verwende ich die Umfragen-Beispiel-App aus dem Tutorial. Fügen Sie ein generisches Fremdschlüsselfeld in das Choice Modell und machen ein neues Thing Modell:

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

Mit einem sauberen db, synchronisiert Tabellen, und erstellen Sie einige Beispiele:

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

So weit, so gut - die Beziehung funktioniert in beiden Richtungen von einer Instanz. Aber von einem queryset ist das Gegenteil der Beziehung sehr seltsam:

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

Warum die inverse Beziehung gibt mir wieder die ID aus den thing? Ich erwartete stattdessen die Primärschlüssel der Wahl, das entspricht dem folgenden Ergebnis:

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

Those ORM-Abfragen generieren SQL wie folgt aus:

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

Die Django-Dokumentation im Allgemeinen ausgezeichnet sind, aber ich kann nicht Verstehen Sie, warum die zweite Abfrage oder finden Sie eine Dokumentation dieses Verhaltens - es scheint, Daten vollständig aus der falschen Tabelle zurückzugeben?

+0

* Anmerkung: * Django Version ist '(1, 7, 11, 'final', 0)'. Ich kann das in Django 1.8 nicht reproduzieren. – wim

+0

Könnte das bedeuten, dass es eine ist, aber in Django 1.7 entschieden sie sich für 1.8 zu reparieren? – mgilson

+0

Möglich, aber ich suchte hoch und niedrig für die Erwähnung in den Versionshinweisen und konnte es nicht finden. Ich nehme an, 'git bisect' könnte es finden .... – wim

Antwort

7

TL; DR Dies war ein Fehler in Django 1.7, der in Django 1.8 behoben wurde.

Der Wechsel zu meistern direkt ging und nicht unter einer deprecation Zeit ging, die nicht zu ist überraschend, da die Aufrechterhaltung der Rückwärtskompatibilität hier sehr schwierig gewesen wäre. Überraschender ist, dass das Problem in der 1.8 release notes nicht erwähnt wurde, da der Fix das Verhalten des gerade aktiven Codes ändert.

Der Rest dieser Antwort ist eine Beschreibung, wie ich das Commit mit git bisect run gefunden habe. Es ist hier für meine eigene Referenz mehr als alles andere, also kann ich hierher zurückkommen, wenn ich jemals wieder ein großes Projekt teilen muss.


Zuerst haben wir einen Django-Klon und ein Testprojekt eingerichtet, um das Problem zu reproduzieren. Ich habe virtualenvwrapper hier verwendet, aber Sie können die Isolierung tun, wie Sie es wünschen.

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 

die folgende Datei erstellen:

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

auch diese:

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

Jetzt haben wir ein Arbeitsprojekt, erstellen Sie die test_script.py mit git bisect run zu verwenden:

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

Das Skript muss ausführbar sein, fügen Sie also das Flag mit chmod +x test_script.py hinzu. Es sollte sich in dem Verzeichnis befinden, in dem Django geklont ist, d. H. /tmp/django/test_script.py für mich. Dies liegt daran, dass import django zuerst das lokal ausgecheckte Django-Projekt und nicht irgendeine Version von Site-Paketen abholen sollte.

wurde die Benutzeroberfläche von git bisect, um herauszufinden, entwickelt, bei denen Fehler erschienen, so die üblichen Präfixe von „schlechten“ und „gut“ sind nach hinten, wenn Sie versuchen, wenn ein bestimmte Fehler fest ist, um herauszufinden,. Dies mag etwas auf den Kopf gestellt erscheinen, aber das Testskript sollte mit Erfolg (Rückkehrcode 0) enden, wenn der Fehler vorhanden ist, und es sollte fehlschlagen (mit Rückgabecode ungleich Null), wenn der Fehler behoben ist. Das hat mich ein paar Mal gestolpert!

Also wird dieser Prozess eine automatisierte Suche durchführen, die schließlich das Commit findet, wo der Fehler behoben wurde. Es dauert einige Zeit, denn zwischen Django 1.7 und Django 1.8 gab es viele Commits. Es halbierte 1362 Revisionen, etwa 10 Stufen, und schließlich Ausgabe:

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. 

, die genau das begehen ist, wo die Abfrage von einer falschen SQL geändert hat (die Daten aus der falschen Tabelle bekommt)

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

in die korrekte Version:

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

natürlich aus dem begeht Hash sind wir in der Lage, die Pull-Anforderung und das Ticket leicht auf github zu finden. Hoffentlich kann dies auch einem anderen Tag helfen - die Zweiteilung von Django kann aufgrund der Migrationen schwierig sein!

1

Bemerkung - zu spät für Antwort - die meisten gelöscht

A nicht wichtiges Ergebnis der Rückwärts unvereinbar fix Ausgabe #24002 ist, dass der GenericRelatedObjectManager (zB things) gestoppt für eine Abfrage arbeitet lange Zeit festgelegt und es verwendet werden könnte, nur für Filter usw.

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

Es wurde später von #24940 in Version 1.8.3 und im Master-Zweig Halbjahr festgelegt. Das Problem war nicht wichtig, weil der generische Name thing einfacher ohne Abfrage (choice.thing) funktioniert und es ist nicht klar, dass diese Verwendung dokumentiert oder nicht dokumentiert ist.

docs: Reverse generic relations:

related_query_name Einstellung schafft eine Beziehung aus dem verwandten Objekt zu diesem zurück.Dies ermöglicht das Abfragen und Filtern von dem verwandten Objekt.

Es wäre nett, wenn der spezifische Beziehungsname anstelle von nur dem generischen verwendet werden könnte. Mit dem Beispiel aus der Dokumentation: taged_item.bookmarks ist besser lesbar als taged_item.content_object, aber es wäre nicht wert zu arbeiten, um es zu implementieren.