Modèles Django – comment filtrer le nombre d’objects ForeignKey

J’ai un modèle A et B , qui sont comme ceci:

 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) 

Maintenant, j’ai quelques objects A et B , et j’aimerais obtenir une requête qui sélectionne tous les objects A qui ont moins de 2 B pointant vers eux.

A est quelque chose comme un pool, et les utilisateurs (le B) se joignent au pool. S’il n’y a que 1 ou 0 joint, le pool ne doit pas être affiché du tout.

Est-ce possible avec une telle conception de modèle? Ou devrais-je le modifier un peu?

Cela ressemble à un travail 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 le compte B est quelque chose dont vous avez souvent besoin comme critère de filtrage ou de classement, ou doit être affiché sur des vues de liste, vous pouvez envisager la dénormalisation en ajoutant un champ b_count à votre modèle A et en utilisant des signaux pour append un B supprimé:

 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) 

Une autre solution serait de gérer un indicateur d'état sur l'object A lorsque vous ajoutez ou supprimez un 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 = ba 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) 

La question et la réponse sélectionnées datent de 2008 et depuis lors, cette fonctionnalité a été intégrée au framework django. Comme il s’agit du plus grand succès de Google pour “décompte des clés étrangères du filtre django”, je voudrais append une solution plus simple avec une version récente de Django utilisant l’ agrégation .

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

Dans mon cas, je devais pousser ce concept plus loin. Mon object “B” avait un champ booléen appelé is_available, et je voulais seulement renvoyer les objects A qui avaient plus de 0 objects B avec is_available défini sur True.

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

Je recommande de modifier votre conception pour inclure un champ de statut sur A.

La question est celle de “pourquoi?” Pourquoi A a-t-il <2 B et pourquoi A a-t-il> = 2 B? Est-ce parce que l’utilisateur n’a pas saisi quelque chose? Ou est-ce parce qu’ils ont essayé et leur entrée a eu des erreurs. Ou est-ce parce que la règle <2 ne s'applique pas dans ce cas.

L’utilisation de la présence ou de l’absence d’une clé étrangère limite la signification à – bien présente ou absente. Vous n’avez aucun moyen de représenter “pourquoi?”

En outre, vous avez l’option suivante

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

Cela peut être coûteux car il récupère tous les A plutôt que de forcer la firebase database à faire le travail.


Edit: À partir du commentaire, "je serais obligé de regarder les utilisateurs rejoindre / quitter les événements du pool".

Vous ne "regardez" rien - vous fournissez une API qui fait ce dont vous avez besoin. C'est le principal avantage du modèle Django. Voici un moyen, avec des méthodes explicites dans la classe 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 

Vous pouvez également définir un Manager spécial pour cela et remplacer le gestionnaire b_set par défaut par votre responsable qui compte les références et les mises à jour.

Je suppose que rejoindre ou quitter le pool peut ne pas arriver aussi souvent que la liste (affichage) des pools. Je pense également qu’il serait plus efficace pour les utilisateurs de joindre / quitter des actions pour mettre à jour l’état d’affichage du pool. De cette façon, la liste et l’affichage des pools nécessiteraient moins de temps, car vous ne feriez qu’une seule requête pour SHOW_STATUS des objects du pool.

Pourquoi ne pas faire comme ça?

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