web-dev-qa-db-ja.com

Djangoモデル-ForeignKeyオブジェクトの数をフィルタリングする方法

モデルABがあり、次のようになっています。

class A(models.Model):
  title = models.CharField(max_length=20)
  (...)

class B(models.Model):
  date = models.DateTimeField(auto_now_add=True)
  (...)
  a = models.ForeignKey(A)

これで、いくつかのAオブジェクトとBオブジェクトがあり、2つ未満のAポインティングを持つすべてのBオブジェクトを選択するクエリを取得したいと思います。それらで。

Aはプールのようなもので、ユーザー(B)はプールに参加します。参加しているのが1つまたは0つしかない場合、プールはまったく表示されません。

そのようなモデルデザインで可能ですか?それとも私はそれを少し変更する必要がありますか?

44
kender

extra の仕事のように聞こえます。

A.objects.extra(
    select={
        'b_count': 'SELECT COUNT(*) FROM yourapp_b WHERE yourapp_b.a_id = yourapp_a.id',
    },
    where=['b_count < 2']
)

Bカウントがフィルタリングまたは順序付けの基準として頻繁に必要なものである場合、またはリストビューに表示する必要がある場合は、Aモデルにb_countフィールドを追加し、Bが追加されたときにシグナルを使用して更新することにより、非正規化を検討できます。削除:

from Django.db import connection, transaction
from Django.db.models.signals import post_delete, post_save

def update_b_count(instance, **kwargs):
    """
    Updates the B count for the A related to the given B.
    """
    if not kwargs.get('created', True) or kwargs.get('raw', False):
        return
    cursor = connection.cursor()
    cursor.execute(
        'UPDATE yourapp_a SET b_count = ('
            'SELECT COUNT(*) FROM yourapp_b '
            'WHERE yourapp_b.a_id = yourapp_a.id'
        ') '
        'WHERE id = %s', [instance.a_id])
    transaction.commit_unless_managed()

post_save.connect(update_b_count, sender=B)
post_delete.connect(update_b_count, sender=B)

別の解決策は、関連するBを追加または削除するときに、Aオブジェクトのステータスフラグを管理することです。

B.objects.create(a=some_a)
if some_a.hidden and some_a.b_set.count() > 1:
    A.objects.filter(id=some_a.id).update(hidden=False)

...

some_a = b.a
some_b.delete()
if not some_a.hidden and some_a.b_set.count() < 2:
    A.objects.filter(id=some_a.id).update(hidden=True)
7
Jonny Buchanan

質問と選択された回答は2008年のものであり、それ以来、この機能はDjangoフレームワークに統合されています。これは「Djangoフィルター外部キーカウント」のトップグーグルヒットであるため、 Aggregation を使用して、最近のDjangoバージョンでより簡単なソリューションを追加します。

from Django.db.models import Count
cats = A.objects.annotate(num_b=Count('b')).filter(num_b__lt=2)

私の場合、この概念をさらに一歩進めなければなりませんでした。私の「B」オブジェクトにはis_availableというブールフィールドがあり、is_availableがTrueに設定された0を超えるBオブジェクトを持つAオブジェクトのみを返したいと思っていました。

A.objects.filter(B__is_available=True).annotate(num_b=Count('b')).filter(num_b__gt=0).order_by('-num_items')
126
gravitron

Aにステータスフィールドを含めるようにデザインを変更することをお勧めします。

問題は「なぜ?」の1つです。 Aに2つ未満のBがあるのはなぜですか、またAに2つ以上のBがあるのはなぜですか。ユーザーが何かを入力しなかったからですか?または、試行して入力にエラーがあったためです。または、この場合、<2ルールが適用されないためです。

外部キーの有無を使用すると、意味が-まあ-存在または不在に制限されます。 「なぜ」を表現する方法がありません。

また、次のオプションがあります

[ a for a in A.objects.all() if a.b_set.count() < 2 ]

これは、データベースに作業を強制するのではなく、すべてのAをフェッチするため、コストがかかる可能性があります。


編集:コメントから「ユーザーの参加/ユーザーがプールイベントを離れるのを監視する必要があります」。

何も「監視」しません。必要なことを実行するAPIを提供します。これがDjangoモデルの中心的な利点です。これが、Aクラスのexplictメソッドを使用する1つの方法です。

class A( models.Model ):
    ....
    def addB( self, b ):
        self.b_set.add( b )
        self.changeFlags()
    def removeB( self, b ):
        self.b_set.remove( b )
        self.changeFlags()
    def changeFlags( self ):
        if self.b_set.count() < 2: self.show= NotYet
        else: self.show= ShowNow

このために特別なManagerを定義し、デフォルトのb_setマネージャーを参照をカウントしてAを更新するマネージャーに置き換えることもできます。

3
S.Lott

プールへの参加またはプールからの離脱は、プールを一覧表示(表示)するほど頻繁には発生しない可能性があると思います。また、ユーザーの参加/離脱アクションがプールの表示ステータスを更新する方が効率的だと思います。このように、プールオブジェクトのSHOW_STATUSに対して単一のクエリを実行するだけなので、プールの一覧表示と表示にかかる時間は短くなります。

1
un33k