2017-10-01 8 views
0

Étant donné un simple ensemble de modèles comme suit:requête Django Annoter définit par des clés étrangères inverse

class A(models.Model): 
    pass 

class B(models.Model): 
    parent = models.ForeignKey(A, related_name='b_set') 

class C(models.Model): 
    parent = models.ForeignKey(B, related_name='c_set') 

Je cherche à créer une requête ensemble de modèle A avec deux annotations. Une annotation doit correspondre au nombre de lignes B dont la ligne A est en question en tant que parent. L'autre annotation doit indiquer le nombre de B lignes, à nouveau avec l'objet A en question en tant que parent, qui ont au moins n objets de type C dans leur c_set.

À titre d'exemple, considérons la base de données suivante et n = 3:

Table A 
id 
0 
1 

Table B 
id parent 
0 0 
1 0 

Table C 
id parent 
0 0 
1 0 
2 1 
3 1 
4 1 

je voudrais être en mesure d'obtenir un résultat de la forme [(0, 2, 1), (1, 0, 0)] comme l'objet A avec id 0 a deux objets dont B on a au moins trois objets connexes C. L'objet A avec l'ID 1 n'a aucun objet B et par conséquent, aucun objet B avec au moins trois lignes C.

La première annotation est trivial:

A.objects.annotate(annotation_1=Count('b_set')) 

Ce que je suis en train de concevoir est maintenant la deuxième annotation. J'ai réussi à compter le nombre de B lignes par A où l'objet B a au moins un objet unique C comme suit:

A.objects.annotate(annotation_2=Count('b_set__c_set__parent', distinct=True)) 

Mais je ne peux pas trouver un moyen de le faire avec une taille de jeu lié au minimum d'autres d'un. J'espère que quelqu'un ici pourra me diriger dans la bonne direction. Une méthode à laquelle je pensais consistait en quelque sorte à annoter les objets B dans la requête au lieu des lignes A comme c'est le cas par défaut de la méthode annotate mais je n'ai trouvé aucune ressource sur ceci.

Répondre

1

Il s'agit d'une requête complexe aux limites de Django 1.11. J'ai décidé de le faire par deux requêtes et de combiner les résultats à une liste qui peut être utilisé par une vue comme un queryset:

from django.db.models import Count 

sub_qs = (
    C.objects 
    .values('parent') 
    .annotate(c_count=Count('id')) 
    .order_by() 
    .filter(c_count__gte=n) 
    .values('parent') 
) 
qs = B.objects.filter(id__in=sub_qs).values('parent_id').annotate(cnt=Count('id')) 
qs_map = {x['parent_id']: x['cnt'] for x in qs} 
rows = list(A.objects.annotate(annotation_1=Count('b_set'))) 
for row in rows: 
    row.annotation_2 = qs_map.get(row.id, 0) 

La liste rows est le résultat. Plus complexe qs.query est compilé à un SQL relativement simple:

>>> print(str(qs.query)) 
SELECT app_b.parent_id, COUNT(app_b.id) AS cnt 
FROM app_b 
WHERE app_b.id IN (
    SELECT U0.parent_id AS Col1 FROM app_c U0 
    GROUP BY U0.parent_id HAVING COUNT(U0.id) >= 3 
) 
GROUP BY app_b.parent_id;    -- (added white space and removed double quotes) 

Cette solution simple peut être modifiée plus facile et testé.


Remarque: Une solution par une requête existe également, mais ne semble pas utile. Pourquoi: Il faudrait Subquery et OuterRef(). Ils sont excellents, mais en général, Count() d'agrégation n'est pas supporté par les requêtes qui sont compilées avec la résolution de jointure. Une sous-requête peut être séparée par la recherche ...__in=... pour pouvoir être compilée par Django, mais il n'est pas possible d'utiliser OuterRef(). Si elle est écrite sans OutRef alors c'est un SQL imbriqué non compliqué si optimal que la complexité temporelle serait probablement O (n) par taille de table A pour beaucoup (ou tous) backends de base de données. Pas testé.