Модели Django - как отфильтровать количество объектов ForeignKey - PullRequest
42 голосов
/ 03 ноября 2008

У меня есть модели A и B, такие как:

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 объектов, и я хотел бы получить запрос, который выбирает все A объекты, которые имеют менее 2 B, указывающих на них.

A - это что-то вроде пула, и пользователи (B) присоединяются к пулу. если присоединено только 1 или 0, пул вообще не должен отображаться.

Возможно ли такое с дизайном модели? Или я должен немного изменить это?

Ответы [ 5 ]

118 голосов
/ 01 июня 2011

Вопрос и выбранный ответ относятся к 2008 году, и с тех пор эта функциональность была интегрирована в среду django. Поскольку это самый популярный продукт в Google для "подсчета внешнего ключа фильтра django", я бы хотел добавить более простое решение с недавней версией django, используя Aggregation .

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

В моем случае мне пришлось пойти дальше этой концепции. Мой объект "B" имел логическое поле с именем is_available, и я хотел только вернуть объекты A, у которых было более 0 объектов B с параметром is_available равным True.

A.objects.filter(B__is_available=True).annotate(num_b=Count('b')).filter(num_b__gt=0).order_by('-num_items')
7 голосов
/ 03 ноября 2008

Звучит как работа для 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 является чем-то, что вам часто требуется в качестве критерия фильтрации или упорядочения, или если оно должно отображаться в представлениях списка, вы можете рассмотреть возможность денормализации, добавив поле b_count в модель A и используя сигналы для его обновления, когда 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)

Другим решением было бы управление флагом состояния объекта A при добавлении или удалении связанного B.

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)
3 голосов
/ 03 ноября 2008

Я бы порекомендовал изменить ваш дизайн, добавив в него поле статуса A.

Вопрос один из "почему?" Почему A имеет <2 B, и почему A имеет> = 2 B. Это потому, что пользователь ничего не вводил? Или потому, что они пытались, и на их входе были ошибки. Или потому, что в этом случае правило <2 не применяется. </p>

Использование наличия или отсутствия внешнего ключа ограничивает значение - хорошо - присутствует или отсутствует. У вас нет никакого способа представить «почему?»

Кроме того, у вас есть следующая опция

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

Это может быть дорого, потому что он выбирает все А, а не заставляет базу данных выполнять работу.


Редактировать: Из комментария "потребовалось бы, чтобы я наблюдал за присоединением пользователя / пользователем, покидающим события пула".

Вы ничего не «смотрите» - вы предоставляете API, который делает то, что вам нужно. Это главное преимущество модели Django. Вот один из способов с использованием методов explict в классе A.

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.

1 голос
/ 10 мая 2009

Я предполагаю, что присоединение или выход из пула может происходить не так часто, как перечисление (показ) пулов. Я также считаю, что для пользователей было бы более эффективно присоединиться к действиям по выходу из системы и обновлять состояние отображения пула. Таким образом, перечисление и отображение пулов потребует меньше времени, так как вы просто выполните один запрос для SHOW_STATUS объектов пула.

0 голосов
/ 03 марта 2017

Как насчет этого?

queryset = A.objects.filter(b__gt=1).distinct()
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...