2008-11-03 15 views
36

tengo unas modelos A y B, que son como esto:modelos de Django - cómo filtrar número de objetos ForeignKey

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) 

Ahora tengo algunas A y B objetos, y me gustaría obtener una consulta que selecciona todos los objetos A que tienen menos de 2 B apuntando a ellos.

A es algo así como una cosa de pool, y los usuarios (los B) se unen al grupo. si solo hay 1 o 0 unidos, el grupo no debería mostrarse en absoluto.

¿Es posible con tal diseño de modelo? ¿O debería modificar eso un poco?

Respuesta

6

Suena como un trabajo 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'] 
) 

Si el recuento de B es algo que a menudo necesita como criterio de filtrado o pedidos, o necesita ser visualizado en vistas de lista, usted podría considerar desnormalización mediante la adición de un campo b_count a su modelo A y el uso de señales para actualizar cuando se añade o se elimina un 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) 

Otra solución sería la de gestionar un indicador de estado en la a objeto cuando se va a añadir o quitar una B. relacionada

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) 
+1

Solución demasiado compleja, mientras que Django tiene la función de anotaciones. –

3

Recomendaría modificar su diseño para incluir algún campo de estado en A.

El problema es uno de "¿por qué?" ¿Por qué A tiene < 2 B y por qué A tiene> = 2 B? ¿Es porque el usuario no ingresó algo? O es porque lo intentaron y su entrada tuvo errores. ¿O es porque la regla < 2 no se aplica en este caso?

El uso de la presencia o ausencia de una clave externa limita el significado a "bien presente o ausente". No tienes forma de representar "¿por qué?"

Además, usted tiene la opción siguiente

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

Esto puede ser caro ya que recupera todas las del lugar de forzar a la base de datos para hacer el trabajo de la Un.


Editar: Desde el comentario "me requeriría mirar para el usuario unirse/usuario dejando los eventos de la piscina".

No "mira" nada; proporciona una API que hace lo que necesita. Ese es el beneficio central del modelo de Django. Aquí hay una forma, con métodos explícitos en la clase 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 

También puede definir una especial Manager para esto, y reemplazar el Administrador predeterminado b_set con su gestor de referencias que cuenta y actualizaciones A.

+0

A es algo así como un elemento de grupo, y los usuarios (los B) unen grupo. si solo hay 1 o 0 unidos, el grupo no debería mostrarse en absoluto ... Es por eso que no quise incluir dicho estado; me requeriría observar si el usuario se unía/el usuario abandonaba los eventos del grupo. Pero, tal vez esa es la manera ... – kender

1

Supongo que unirse o abandonar el grupo puede no ocurrir tan a menudo como mostrar (mostrar) los grupos. También creo que sería más eficiente para los usuarios unirse/dejar acciones para actualizar el estado de visualización de la agrupación.De esta manera, enumerar & mostrando las agrupaciones requeriría menos tiempo ya que solo ejecutaría una única consulta para SHOW_STATUS de los objetos de la agrupación.

104

La pregunta y la respuesta seleccionada son de 2008 y desde entonces esta funcionalidad se ha integrado en el marco django. Dado que este es uno de los principales resultados de google para "filtro de django cuenta de clave externa", me gustaría agregar una solución más fácil con una versión reciente de django utilizando Aggregation.

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

En mi caso, tuve que llevar este concepto un paso más allá. Mi objeto "B" tenía un campo booleano llamado is_available, y solo quería devolver los objetos A que tenían más de 0 objetos B con is_available establecido en True.

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

He intentado hacer esto, pero es muy lento (como usar el "extra" en la respuesta 1 fue casi instantáneo, pero con esto le tomó a cada entrada más de 5 segundos). ¿Hay alguna manera de que sea más rápido sin caer en SQL sin formato? – daveeloo

+0

Lo siento, todavía no he tenido que lidiar con una gran cantidad de datos que llegan a mi sitio, por lo que no he tratado aún de agilizar esta consulta. Sugeriría almacenar en caché el resultado y solo ejecutar la consulta periódicamente. – gravitron

+6

Agregando SQL extra extra "se rompe" la idea de utilizar un ORM – llazzaro

0

¿Qué tal si hacemos esto?

queryset = A.objects.filter(b__gt=1).distinct() 
Cuestiones relacionadas