modelos do Django - como filtrar número de objetos ForeignKey

votos
37

Eu tenho modelos Ae B, que são assim:

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)

Agora eu tenho alguns Ae Bobjetos, e eu gostaria de ter uma consulta que seleciona todos os Aobjetos que têm menos de 2 Bapontando para eles.

Um é algo como uma coisa piscina, e os usuários (o B) se juntar a piscina. se há apenas 1 ou 0 se juntou, a piscina não deve ser exibido em tudo.

É possível com tal design do modelo? Ou devo modificar isso um pouco?

Publicado 03/11/2008 em 11:38
fonte usuário
Em outras línguas...                            


5 respostas

votos
107

A pergunta e resposta selecionada são de 2008 e, desde então, essa funcionalidade foi integrado no âmbito Django. Uma vez que este é um top google hit de "Django filtrar contagem de chave estrangeira" Eu gostaria de adicionar uma solução mais fácil com uma versão recente Django usando Agregação .

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

No meu caso eu tive que tomar esse conceito um passo adiante. Meu objetivo "B" teve um campo boolean chamado is_available, e eu só queria retornar uma objetos que tinham mais de 0 B objetos com set is_available para True.

A.objects.filter(B__is_available=True).annotate(num_b=Count('b')).filter(num_b__gt=0).order_by('-num_items')
Respondeu 01/06/2011 em 18:30
fonte usuário

votos
7

Soa como um trabalho para extra.

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

Se a contagem B é algo que muitas vezes precisam de um critério de filtragem ou ordenação, ou precisa ser exibido em vistas de lista, você poderia considerar desnormalização adicionando um campo b_count ao seu modelo A e utilizando sinais para atualizá-lo quando um B é adicionado ou excluída:

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)

Outra solução seria para gerenciar um sinalizador de status no objeto A quando você está adicionando ou removendo um B. relacionado

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)
Respondeu 03/11/2008 em 11:47
fonte usuário

votos
3

Eu recomendo modificar o seu projeto para incluir algum campo de status em A.

A questão é um dos "porquê?" Por que A tem <2 B e por que um tem> = 2 B do. É porque o usuário do não entrou alguma coisa? Ou é porque eles tentaram e sua entrada teve erros. Ou é porque a regra <2 não se aplica neste caso.

Usando presença ou ausência de uma chave estrangeira limita o significado de - bem - presente ou ausente. Você não tem qualquer maneira de representar "por quê?"

Além disso, você tem a opção a seguir

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

Isto pode ser caro, porque não buscar todos do em vez de forçar o banco de dados para fazer o trabalho A.


Edit: A partir do comentário "exigiria-me para assistir para o usuário se juntar / utilizador deixando os eventos de piscina".

Você não "assistir" qualquer coisa - você fornece uma API que faz o que você precisa. Esse é o benefício central do modelo Django. Aqui está uma maneira, com métodos explict na Aclasse.

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

Você também pode definir um especial Managerpara isso, e substituir o padrão b_setManager com o seu gerente que conta referências e atualizações A.

Respondeu 03/11/2008 em 12:00
fonte usuário

votos
1

Presumo que entrar ou sair da piscina pode não acontecer tão frequentemente como listando (mostrando) as piscinas. Eu também acredito que seria mais eficiente para os usuários se juntar / deixar ações para atualizar o status de exibição piscina. Desta forma, listando e mostrando as piscinas exigiria menos tempo que você teria apenas que executar uma única consulta para SHOW_STATUS dos objetos piscina.

Respondeu 10/05/2009 em 19:26
fonte usuário

votos
0

Como apenas cerca de fazer dessa maneira?

queryset = A.objects.filter(b__gt=1).distinct()
Respondeu 03/03/2017 em 04:42
fonte usuário

Cookies help us deliver our services. By using our services, you agree to our use of cookies. Learn more