what - tutorial de django
django comenta y cuenta: cómo filtrar los que se incluirán en la cuenta (3)
Dado un queryset, agrego el recuento de objetos relacionados (Modelo A) con lo siguiente:
qs = User.objets.all()
qs.annotate(modela__count=models.Count(''modela''))
Sin embargo, ¿hay una manera de contar el Modelo A que solo cumpla con un criterio? Por ejemplo, cuente el ModelA donde deleted_at es nulo?
He intentado dos soluciones que no funcionan correctamente.
1) Como sugirió @knbk, use el filtro antes de anotar.
qs = User.objects.all().filter(modela__deleted_at__isnull=True).annotate(modela__count=models.Count(''modela'', distinct=True))
Aquí está la versión simplificada de la consulta generada por django:
SELECT COUNT(DISTINCT "modela"."id") AS "modela__count", "users".*
FROM "users"
LEFT OUTER JOIN "modela" ON ( "users"."id" = "modela"."user_id" )
WHERE "modela"."deleted_at" IS NULL
GROUP BY "users"."id"
El problema viene de la cláusula WHERE. De hecho, hay un IZQUIERDA ÚNICA, pero las últimas condiciones DÓNDE la obligaron a ser una ÚNICA ÚNICA. Necesito poner las condiciones en la cláusula JOIN para que funcione según lo previsto.
Entonces, en lugar de
LEFT OUTER JOIN "modela" ON ( "users"."id" = "modela"."user_id" )
WHERE "modela"."deleted_at" IS NULL
Necesito lo siguiente que funciona cuando lo ejecuto directamente en SQL simple.
LEFT OUTER JOIN "modela" ON ( "users"."id" = "modela"."user_id" )
AND "modela"."deleted_at" IS NULL
¿Cómo puedo cambiar el queryset para obtener esto sin hacer una consulta en bruto?
2) Como otros sugirieron, podría usar una agregación condicional.
Intenté lo siguiente:
qs = User.objects.all().annotate(modela__count=models.Count(Case(When(modela__deleted_at__isnull=True, then=1))))
que se convierte en la siguiente consulta SQL:
SELECT COUNT(CASE WHEN "modela"."deleted_at" IS NULL THEN 1 ELSE NULL END) AS "modela__count", "users".*
FROM "users" LEFT OUTER JOIN "modela" ON ( "users"."id" = "modela"."user_id" )
GROUP BY "users"."id"
Al hacer eso, recibo a todos los usuarios (por lo tanto, la IZQUIERDA A LA IZQUIERDA funciona correctamente) pero obtengo "1" (en lugar de 0) para modela__count
para todos los usuarios que no tienen ningún ModelA. ¿Por qué obtengo 1 y no 0 si no hay nada que contar? ¿Cómo se puede cambiar eso?
En Django 1.8 creo que esto se puede lograr con la agregación condicional . Sin embargo para versiones anteriores lo haría con .extra
ModelA.objects.extra(select={
''account_count'': ''SELECT COUNT(*) FROM account WHERE modela.account_id = account.id AND account.some_prop IS NOT NULL''
})
En una modela
LEFT JOIN
, todos los campos de modela
podrían ser NULL
debido a la ausencia de la fila correspondiente. Asi que
modela.deleted_at IS NULL
... no solo es cierto para las filas coincidentes, sino también para aquellos users
que no modela
filas de modela
correspondiente.
Creo que el SQL correcto debería ser:
SELECT COUNT(
CASE
WHEN
`modela`.`user_id` IS NOT NULL -- Make sure modela rows exist
AND `modela`.`deleted_at` IS NULL
THEN 1
ELSE NULL
END
) AS `modela__count`,
`users`.*
FROM `users`
LEFT OUTER JOIN `modela`
ON ( `users`.`id` = `modela`.`user_id` )
GROUP BY `users`.`id`
En Django 1.8 esto debería ser:
from django.db import models
qs = User.objects.all().annotate(
modela_count=models.Count(
models.Case(
models.When(
modela__user_id__isnull=False,
modela__deleted_at__isnull=True,
then=1,
)
)
)
)
Aviso :
@YAmikep descubrió que un error en Django 1.8.0 hace que el SQL generado tenga un INNER JOIN
lugar de un LEFT JOIN
, por lo que perderá filas sin la correspondiente relación de clave externa. Utilice la versión Django 1.8.2 o superior para solucionarlo.
Simplemente puede filtrar antes de anotar:
from django.db.models import Q, Count
qs = ModelA.objects.filter(account__prop1__isnull=False).annotate(account_count=Count(''account''))