13

Django のコンテンツ タイプ フレームワークのGenericRelationフィールドについて、私は本当に誤解しているに違いありません。

最小限の自己完結型の例を作成するために、チュートリアルの polls サンプル アプリを使用します。モデルに一般的な外部キー フィールドを追加し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]

逆の関係が からの 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のドキュメントは一般的に優れていますが、2番目のクエリがなぜその動作のドキュメントを見つけるのか理解できません.間違ったテーブルから完全にデータを返すようです?

4

2 に答える 2

8

TL;DRこれは Django 1.7 のバグで、Django 1.8 で修正されました。

変更は master に直接適用され、非推奨期間にはなりませんでした。ここで下位互換性を維持することは非常に困難であったことを考えると、これはそれほど驚くべきことではありません。さらに驚くべきことに、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)

スクリプトは実行可能である必要があるため、フラグをchmod +x test_script.py. これは、Django のクローンが作成されたディレクトリ (つまり/tmp/django/test_script.py、私の場合) に配置する必要があります。これはimport django、site-packages からのバージョンではなく、ローカルでチェックアウトされた django プロジェクトを最初に取得する必要があるためです。

git bisect のユーザー インターフェイスは、バグが発生した場所を見つけるように設計されているため、特定のバグがいつ修正されたかを確認しようとする場合、通常の「悪い」と「良い」の接頭辞は逆になります。これは逆さまに見えるかもしれませんが、テスト スクリプトは、バグが存在する場合は成功 (戻りコード 0) で終了し、バグが修正されている場合は失敗します (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を二分することは、移行のためにセットアップが難しい場合があります.

于 2016-10-09T16:25:25.027 に答える
2

コメント - 回答が遅すぎる - ほとんどが削除された

問題#24002の下位互換性のない修正の重要ではない結果は、GenericRelatedObjectManager (例: things) がクエリ セットに対して長い間機能しなくなり、フィルターなどにしか使用できなくなったことです。

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

半年後、バージョン 1.8.3 およびマスター ブランチで#24940によって修正されました。一般的な名前thingはクエリ (choice.thing) なしで簡単に機能し、この使用法が文書化されているか文書化されていないかが明確ではないため、問題は重要ではありませんでした。

docs:逆ジェネリック関係:

設定related_query_nameにより、関連オブジェクトからこのオブジェクトへの関係が作成されます。これにより、関連オブジェクトからのクエリとフィルタリングが可能になります。

ジェネリックのみの代わりに特定のリレーション名を使用できるとよいでしょう。docs: の例でtaged_item.bookmarksは よりも読みやすいですがtaged_item.content_object、実装する価値はありません。

于 2016-10-09T21:01:17.200 に答える