web-dev-qa-db-ja.com

Djangoで複数のfilter()をチェーンする、これはバグですか?

私は常に、Djangoで複数のfilter()呼び出しをチェーンすることは、1回の呼び出しでそれらを収集することと常に同じであると仮定しました。

_# Equivalent
Model.objects.filter(foo=1).filter(bar=2)
Model.objects.filter(foo=1,bar=2)
_

しかし、私は私のコードで複雑なクエリセットに遭遇しましたが、そうではありません

_class Inventory(models.Model):
    book = models.ForeignKey(Book)

class Profile(models.Model):
    user = models.OneToOneField(auth.models.User)
    vacation = models.BooleanField()
    country = models.CharField(max_length=30)

# Not Equivalent!
Book.objects.filter(inventory__user__profile__vacation=False).filter(inventory__user__profile__country='BR')
Book.objects.filter(inventory__user__profile__vacation=False, inventory__user__profile__country='BR')
_

生成されたSQLは

_SELECT "library_book"."id", "library_book"."asin", "library_book"."added", "library_book"."updated" FROM "library_book" INNER JOIN "library_inventory" ON ("library_book"."id" = "library_inventory"."book_id") INNER JOIN "auth_user" ON ("library_inventory"."user_id" = "auth_user"."id") INNER JOIN "library_profile" ON ("auth_user"."id" = "library_profile"."user_id") INNER JOIN "library_inventory" T5 ON ("library_book"."id" = T5."book_id") INNER JOIN "auth_user" T6 ON (T5."user_id" = T6."id") INNER JOIN "library_profile" T7 ON (T6."id" = T7."user_id") WHERE ("library_profile"."vacation" = False  AND T7."country" = BR )
SELECT "library_book"."id", "library_book"."asin", "library_book"."added", "library_book"."updated" FROM "library_book" INNER JOIN "library_inventory" ON ("library_book"."id" = "library_inventory"."book_id") INNER JOIN "auth_user" ON ("library_inventory"."user_id" = "auth_user"."id") INNER JOIN "library_profile" ON ("auth_user"."id" = "library_profile"."user_id") WHERE ("library_profile"."vacation" = False  AND "library_profile"."country" = BR )
_

チェーンされたfilter()呼び出しを持つ最初のクエリセットは、2つの条件の間にORを効果的に作成するインベントリモデルを2回結合しますが、2番目のクエリセットは2つの条件をAND最初のクエリは、2つの条件もANDしますこれは予想される動作ですか、これはDjangoのバグですか?

関連する質問への回答 Djangoで ".filter()。filter()。filter()..."を使用することのマイナス面はありますか? は、2つのクエリセットが同等であることを示しているようです。

77
user27478

私がそれを理解する方法は、それらが設計によって微妙に異なることです(そして、私は確かに修正のために開いています):filter(A, B)は最初にAに従ってフィルタリングし、次にBに従ってサブフィルタリングしますが、filter(A).filter(B)は、Aに一致する行と、Bに一致する潜在的に異なる行を返します。

ここの例を見てください:

https://docs.djangoproject.com/en/dev/topics/db/queries/#spanning-multi-valued-relationships

特に:

単一のfilter()呼び出し内のすべてが同時に適用され、これらすべての要件に一致するアイテムが除外されます。連続したfilter()呼び出しは、オブジェクトのセットをさらに制限します

...

この2番目の例(filter(A).filter(B))では、最初のフィルターはクエリセットを(A)に制限しました。 2番目のフィルターは、ブログのセットを(B)のブログにさらに制限しました。 2番目のフィルターによって選択されたエントリは、最初のフィルターのエントリと同じ場合と異なる場合があります。

86
Timmy O'Mahony

これらの2つのスタイルのフィルタリングはほとんどの場合同等ですが、ForeignKeyまたはManyToManyFieldに基づいたオブジェクトのクエリでは、わずかに異なります。

ドキュメント の例。

モデル
Blog to Entryは1対多の関係です。

from Django.db import models

class Blog(models.Model):
    ...

class Entry(models.Model):
    blog = models.ForeignKey(Blog)
    headline = models.CharField(max_length=255)
    pub_date = models.DateField()
    ...

オブジェクト
ここにいくつかのブログとエントリーのオブジェクトがあると仮定します。
enter image description here

クエリ

Blog.objects.filter(entry__headline_contains='Lennon', 
    entry__pub_date__year=2008)
Blog.objects.filter(entry__headline_contains='Lennon').filter(
    entry__pub_date__year=2008)  

最初のクエリ(単一のフィルタ1)では、blog1のみに一致します。

2番目のクエリ(チェーンフィルタ1)については、blog1とblog2を除外します。
最初のフィルターは、クエリセットをblog1、blog2、blog5に制限します。 2番目のフィルターは、ブログのセットをさらにblog1とblog2に制限します。

そして、あなたはそれを理解する必要があります

エントリーアイテムではなく、各フィルターステートメントでブログアイテムをフィルタリングしています。

ブログとエントリーは多値の関係であるため、同じではありません。

リファレンス: https://docs.djangoproject.com/en/1.8/topics/db/queries/#spanning-multi-valued-relationships
何か問題がある場合は、修正してください。

編集:1.6リンクが使用できなくなったため、v1.6をv1.8に変更しました。

50
Kevin_wyx

生成されたSQLステートメントを見るとわかるように、違いは「OR」ではありません。 WHEREとJOINの配置方法です。

Example1(同じ結合テーブル):

https://docs.djangoproject.com/en/dev/topics/db/queries/#spanning-multi-valued-relationships )の例)

Blog.objects.filter(entry__headline__contains='Lennon', entry__pub_date__year=2008)

これにより、(entry _headline _ contains = 'Lennon')AND(entry__pub_date__year)の両方を持つoneエントリを持つすべてのブログが得られます= 2008)、これはこのクエリから予想されるものです。結果:{entry.headline: 'Life of Lennon'、entry.pub_date: '2008'}の本

例2(連鎖)

Blog.objects.filter(entry__headline__contains='Lennon').filter(entry__pub_date__year=2008)

これにより、例1のすべての結果がカバーされますが、生成される結果はわずかに多くなります。最初に(entry _headline _ contains = 'Lennon')ですべてのブログをフィルタリングし、次に結果フィルター(entry__pub_date__year = 2008)でフィルタリングするためです。

違いは、次のような結果も得られることです:Book with {entry.headline: 'Lennon'、entry.pub_date:2000}、{entry .headline: 'Bill'、entry.pub_date:2008}

あなたの場合

私はあなたが必要とするこれだと思う:

Book.objects.filter(inventory__user__profile__vacation=False, inventory__user__profile__country='BR')

ORを使用する場合: https://docs.djangoproject.com/en/dev/topics/db/queries/#complex-lookups -with-q-objects

5
Johnny Tsang

次のように、複数のフィルターを結合したくない場合があります。

def your_dynamic_query_generator(self, event: Event):
    qs \
    .filter(shiftregistrations__event=event) \
    .filter(shiftregistrations__shifts=False)

そして、次のコードは実際には正しいものを返しません。

def your_dynamic_query_generator(self, event: Event):
    return Q(shiftregistrations__event=event) & Q(shiftregistrations__shifts=False)

今できることは、注釈カウントフィルターを使用することです。

この場合、特定のイベントに属するすべてのシフトをカウントします。

qs: EventQuerySet = qs.annotate(
    num_shifts=Count('shiftregistrations__shifts', filter=Q(shiftregistrations__event=event))
)

その後、注釈でフィルタリングできます。

def your_dynamic_query_generator(self):
    return Q(num_shifts=0)

このソリューションは、大規模なクエリセットでも安価です。

お役に立てれば。

0
Tobias Ernst