12

Devo davvero fraintendere qualcosa con lo GenericRelation field dal framework dei tipi di contenuto di Django.Come utilizzare l'inverso di una GenericRelation

Per creare un esempio autonomo minimo, userò l'applicazione di esempio dei sondaggi del tutorial. Aggiungere un campo chiave esterna generico nel modello Choice, e fare una nuova Thing modello:

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

Con un db pulito, sincronizzati su tabelle e creare alcuni casi:

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

Fin qui tutto bene - la relazione funziona in entrambe le direzioni da un'istanza. Ma da un set di query, la relazione inversa è molto strano:

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

Perché la relazione inversa mi dà di nuovo l'id dalla thing? Mi aspettavo invece la chiave primaria della scelta, equivalente al seguente risultato:

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

le query SQL ORM generare in questo modo:

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

La documentazione Django sono generalmente eccellenti, ma non posso capire perché la seconda query o trovare alcuna documentazione di tale comportamento - sembra di restituire completamente i dati dalla tabella sbagliata?

+0

* nota: * La versione di Django è '(1, 7, 11, 'finale', 0)'. Non riesco a riprodurre questo in Django 1.8. – wim

+0

Potrebbe significare che è un ma in Django 1.7 che hanno deciso di risolvere per 1.8? – mgilson

+0

Possibile, ma ho cercato in alto e in basso per la menzione nelle note di rilascio e non riuscivo a trovarlo. Suppongo che 'git bisect' possa trovarlo ... – wim

risposta

7

TL; DR Questo era un bug in Django 1.7 che è stato corretto in Django 1.8.

Il cambiamento è andato direttamente da padroneggiare e non è andato sotto un periodo di deprecazione, che non è troppo sorprendente dato che mantenere la retrocompatibilità qui sarebbe stato davvero difficile. Più sorprendente è che non è stato menzionato il problema nello 1.8 release notes, poiché la correzione modifica il comportamento del codice attualmente funzionante.

Il resto di questa risposta è una descrizione di come ho trovato il commit utilizzando git bisect run. È qui per il mio riferimento più di ogni altra cosa, quindi posso tornare qui se ho bisogno di rielaborare nuovamente un grande progetto.


Per prima cosa installiamo un clone di django e un progetto di test per riprodurre il problema. Ho usato virtualenvwrapper qui, ma puoi isolare come preferisci.

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 

creare il seguente file:

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

anche questo:

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

Ora abbiamo un progetto di lavoro, creare la test_script.py da usare con 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) 

Lo script deve essere eseguibile, quindi aggiungere il flag con chmod +x test_script.py. Dovrebbe trovarsi nella directory in cui è stato clonato Django, ovvero /tmp/django/test_script.py per me. Questo perché import django dovrebbe prima prelevare il progetto django verificato a livello locale, non tutte le versioni dai pacchetti del sito.

L'interfaccia utente di bisect git è stato progettato per scoprire dove i bug apparsi, in modo che i consueti prefissi di "cattivo" e "buono" sono indietro quando si sta cercando di scoprire quando un certo bug era fisso. Questo potrebbe sembrare un po 'sottosopra, ma lo script di test dovrebbe uscire con successo (codice di ritorno 0) se il bug è presente, e dovrebbe fallire (con codice di ritorno diverso da zero) se il bug è corretto. Questo mi ha fatto inciampare alcune volte!

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 

Quindi questo processo farà una ricerca automatica, che alla fine trova il commit in cui il bug è stato risolto. Ci vuole un po 'di tempo, perché c'erano un sacco di commit tra Django 1.7 e Django 1.8. E 'diviso in due 1362 revisioni, circa 10 gradini, e alla fine di uscita:

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. 

Questo è precisamente il commit in cui la query è cambiato da SQL non corretta (che ottiene i dati dalla tabella sbagliata)

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 la versione corretta:

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

Naturalmente, dalla hash commettere siamo in grado di trovare la richiesta di pull e il biglietto facilmente su GitHub. Speriamo che questo possa aiutare qualcun altro un giorno anche - biseggere Django può essere complicato da configurare a causa delle migrazioni!

1

commento - troppo tardi per la risposta - la maggior parte cancellato

A non importante risultato della correzione incompatibile all'indietro di emissione #24002 è che il GenericRelatedObjectManager (ad es things) ha smesso di funzionare per una query set molto tempo e potrebbe essere usato solo per i filtri, ecc

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

e 'stato fissato a metà anno dopo da #24940 nella versione 1.8.3 e nel ramo master. Il problema non era importante perché il nome generico thing funziona più facilmente senza query (choice.thing) e non è chiaro che questo utilizzo sia documentato o non documentato.

docs: Reverse generic relations:

Impostazione related_query_name crea una relazione dal relativo oggetto di nuovo a questo.Ciò consente l'interrogazione e il filtraggio dall'oggetto correlato.

Sarebbe bello se fosse possibile utilizzare il nome specifico della relazione anziché solo il generico. Con l'esempio dei documenti: taged_item.bookmarks è più leggibile di taged_item.content_object, ma non varrebbe la pena di implementarlo.