Djangoで複数のfilter()を連鎖させると、これはバグですか?


103

私はいつも、Djangoで複数のfilter()呼び出しをチェーンすることは、それらを単一の呼び出しで収集することと常に同じであると想定していました。

# 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回のクエリセットのAND条件を一緒にANDする一方で、2回効果的にインベントリモデルを結合します。最初のクエリも2つの条件のANDになると期待していました。これは予想される動作ですか、それともDjangoのバグですか?

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

回答:


117

私が理解している方法は、設計によって微妙に異なるということです(そして私は間違いなく修正のfilter(A, B)余地があります):最初にAに従ってフィルタリングし、次にBに従ってサブフィルタリングしますが、Aにfilter(A).filter(B)一致する行を返しますBに一致する行。

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

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

特に:

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

...

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


18
この動作は、文書化されていますが、最小の驚きの原則に違反しているようです。複数のfilter()は、フィールドが同じモデルにある場合はANDで結合されますが、関係をまたがる場合はORで結合されます。
gerdemb

3
私はあなたがそれを最初の段落で間違った方法で持っていると信じています-filter(A、B)はAND状況(ドキュメントの「lennon」AND 2008)であり、filter(A).filter(B)はOR状況です( 'lennon' OR 2008)。これは、質問で生成されたクエリを見ると理にかなっています。.filter(A).filter(B)の場合、結合が2回作成され、ORになります。
Sam

17
filter(A、B)はAND filter(A).filter(B)はOR
WeizhongTu

3
そのfurther restrict手段はless restrictive
2016

7
この答えは間違っています。「OR」ではありません。この文は、「2番目のフィルターにより、一連のブログを(B)のブログにさらに制限しました。」「(B)でもある」と明記されている。この特定の例でORに類似した動作が見られる場合、必ずしも独自の解釈を一般化できるとは限りません。「ケビン3112」「ジョニー・ツァン」の答えをご覧ください。正解だと思います。
16

66

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

ドキュメントの例。


エントリからブログへのモデルは、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()
    ...

オブジェクト
ここにいくつかのブログとエントリーオブジェクトがあると仮定します。
ここに画像の説明を入力してください

クエリ

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に変更しました。


3
「マッチ」と「フィルターアウト」は混同されているようです。「このクエリが返す」にこだわっていれば、はるかに明確になります。
OrangeDog 2018年

7

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

例1(同じ結合テーブル):

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 = 2008)の両方を含む1つのエントリを持つすべてのブログが得られます。これは、このクエリから予想されるものです。結果:{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)をフィルターするためです。

ブック:違いは、それはまたあなたを与えるような結果になるということである{entry.headline: ' レノン '、entry.pub_date:2000}、{entry.headline: 'ビル'、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をお読みください。


2番目の例は実際には当てはまりません。チェーンされたすべてのフィルターは、照会されたオブジェクトに適用されます。
Janne

例2は正しいと思いますが、これは実際には公式のDjangoドキュメントから引用した説明です。私は最高の説明者ではないかもしれませんが、私はそれを許します。例1は、通常のSQLの記述で予想されるような直接ANDです。例1は、このような何かを与える: 'SELECTブログはentry.head_line LIKE "エントリ登録しよレノン " AND entry.year == 2008例2はこのような何かを与えた:' SELECTブログエントリを登録しようWHERE entry.head_list LIKE " レノン " UNION SELECTのブログJOINエントリWHERE entry.head_list LIKE " Lennon " '
Johnny Tsang

サー、あなたは全く正しいです。急いで、フィルター基準がブログ自体ではなく、1対多の関係を指しているという事実を見落としました。
Janne

0

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

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)

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

お役に立てれば。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.