2011-01-06 7 views
19

¿Es posible filtrar dentro de una anotación?Anotación de Django con filtro anidado

En mi mente algo como esto (que en realidad no funciona)

Student.objects.all().annotate(Count('attendance').filter(type="Excused"))

La tabla resultante tendría a cada estudiante con el número de ausencias justificadas. Mirar a través de los filtros de documentación solo puede ser antes o después de la anotación, lo que no daría los resultados deseados.

Una solución es esta

for student in Student.objects.all(): 
    student.num_excused_absence = Attendance.objects.filter(student=student, type="Excused").count() 

Esto funciona, pero hace muchas preguntas, en una aplicación real esto puede ser excesivamente largo. Creo que este tipo de declaración es posible en SQL pero preferiría permanecer con ORM si es posible. Incluso intenté hacer dos consultas separadas (una para todos los estudiantes, otra para obtener el total) y las combiné con |. La combinación del total cambió :(

Algunas reflexiones después de la lectura de las respuestas y los comentarios

He resuelto el problema de asistencia utilizando SQL adicional here.

  • Timmy's blog post era útil. Mi respuesta se basa fuera de ella.
  • La respuesta de hash1baby funciona pero parece tan compleja como sql. También requiere ejecutar sql y luego agregar el resultado en un bucle for. Esto es malo para mí porque estoy combinando muchas de estas consultas de filtrado. Mi solución construye un gran queryset w con muchos filtros y más y lo ejecuta todo a la vez.
  • Si el rendimiento no es un problema, sugiero que el bucle for funcione. Es de lejos el más fácil de entender.
+3

He escrito una publicación en el blog sobre esto, ya que no es posible (afaik) sin utilizar SQL sin formato o simplemente iterar sobre los resultados: http://timmyomahony.com/blog/filtering-annotations-django/ –

+0

Hay una nueva solución para esto en el ORM a partir de Django 1.8. Ver mi respuesta a continuación. – gordonc

Respuesta

13

A partir de Django 1.8 puede hacerlo directamente en el ORM:

students = Student.objects.all().annotate(num_excused_absences=models.Sum(
    models.Case(
     models.When(absence__type='Excused', then=1), 
    default=0, 
    output_field=models.IntegerField() 
))) 

respuesta adaptada de another SO question on the same topic

no he probado el ejemplo anterior, pero sí logró algo similar en mi propia aplicación.

-3

Tal vez esto va a funcionar para usted:

excused = Student.objects.filter(attendance__type='Excused').annotate(abs=Count('attendance')) 

necesita filtrar los estudiantes que está buscando primero solo los que tienen ausencias justificadas y luego anotar la cuenta de ellos.

Aquí hay un enlace al Django Aggregation Docs donde se analiza el orden de filtrado.

+0

El problema aquí está justificado ahora es una lista de estudiantes con al menos una ausencia justificada. Como mencioné, pensé que tal vez había alguna manera de combinar esto, pero no he podido pensar en una forma de hacerlo. El "|" puede combinar los conjuntos de consulta, pero elimina efectivamente el filtro, lo que cambia el recuento. – Bufke

+0

Supongo que no entendí los requisitos correctos. Estaba mirando "La tabla resultante tendría a cada alumno con el número de ausencias justificadas". ¿Está buscando tener un conjunto de resultados que tenga una entrada para cada combinación estudiante/asistencia__? – RyanBrady

+0

Sí, como este estudiante | Total de ausencias justificadas | Ausencias totales | Total Tardanzas, etc. – Bufke

5

Tiene la razón: django no le permite filtrar los objetos relacionados que se están contando, sin aplicar también el filtro a los objetos primarios y, por lo tanto, excluyendo aquellos objetos primarios que no tienen objetos relacionados después del filtrado.

Pero, en una pequeña fuga de abstracción, puede contar grupos usando una consulta de valores.

Por lo tanto, recojo las ausencias en un diccionario, y lo uso en un bucle. Algo como esto:

# a query for students 
students = Students.objects.all() 
# a query to count the student attendances, grouped by type. 
attendance_counts = Attendence(student__in=students).values('student', 'type').annotate(abs=Count('pk')) 
# regroup that into a dictionary {student -> { type -> count }} 
from itertools import groupby 
attendance_s_t = dict((s, (dict(t, c) for (s, t, c) in g)) for s, g in groupby(attendance_counts, lambda (s, t, c): s)) 
# then use them efficiently: 
for student in students: 
    student.absences = attendance_s_t.get(student.pk, {}).get('Excused', 0) 
+0

¿No estaría esto sujeto a una condición de carrera si los datos cambian entre la línea 2 y la línea 4? – Gelatin

Cuestiones relacionadas