Django es el entorno de desarrollo web para perfeccionistas con límites de tiempo

Entradas sobre "querysets":

Ordenar una query por la longitud de un campo

A veces tenemos que hacer queries un poco especiales y necesitamos ayuda de SQL cuando el ORM de Django se nos queda pequeño. Aquí tenemos un ejemplo de cómo ordenar una query a la base de datos por la longitud de un campo:

Modelo.objects.all().extra(select={'length':'Length(campo)'}).order_by('length')

Aprovechando la función extra del ORM de Django que nos permite añadir SQL a nuestras queries podemos calcular la longitud del campo con la función Length() de SQL y guardarla en un argumento length que luego utilizamos en el order_by.

Publicado por Antonio Melé el Jueves 13 d Octubre d 2011 | 1 comentario | Categorías: querysets, trucos

Aplicar filtros a annotate() al usar Count()

A veces queremos utilizar la función de aggregación annotate() y aplicar filtros al modelo que se encuentra dentro de la misma, pero no es posible hacerlo con el ORM de Django actualmente. En este caso la solución pasa por usar la función extra() del ORM de Django para incluir SQL propio.

Vamos a ver un ejemplo práctico. Supongamos una aplicación llamada ocio y el siguiente models.py con hobbies y personas que los practican:

from django.db import models

class Hobby(models.Model):
    nombre = models.CharField(max_length=250)

SEXO_CHOICES = (
    ('m','Masculino'),
    ('f','Femenino'),
)

class Persona(models.Model):
    nombre = models.CharField(max_length=250)
    sexo = models.CharField(max_length=1, choices=SEXO_CHOICES)
    hobbies = models.ManyToManyField(Hobby, related_name='personas')

Gracias a las funciones de agregación de Django podemos obtener fácilmente todos los hobbies y el número de personas que lo practican utilizando annotate(). En el siguiente ejemplo vemos que para cada hobby se contará el número de personas relacionadas y será accesible mediante el atributo personas_count:

from django.db.models import Count

hobbies = Hobby.objects.annotate(personas_count=Count('personas_count'))

for hobby in hobbies:
    print '%s personas practican %s' % (hobby.personas_count, hobby.nombre)

Con esto contamos todas las personas relacionadas con cada hobby, pero a veces nos interesa filtrar las personas que se contabilizan. ¿Y si sólo quisiéramos obtener el número de mujeres que practican cada hobby? En este caso queremos filtrar por un atributo del modelo que se está utilizando en annotate() y de momento no se puede hacer con el ORM de Django.

Para obtener el número de mujeres que practican cada hobby lo primero que haremos será obtener mediante values_list() un listado de los id de las personas que cumplan la condición sexo='f':

# obtenemos lista en python, p. ej: [13, 21, 60]
mujeres_ids = Persona.objects.filter(sexo='f').values_list('id', flat=True)

A continuación convertiremos la lista a un string con id's separados por comas para poder utilizarlos en nuestra sentencia SQL:

# id's separados por comas en string, p. ej: "13,21,60"
lista_ids = ','.join(str(id) for id in mujeres_ids)

Después prepararemos la sentencia SQL que utilizaremos en nuestro queryset(). Recordemos que por defecto Django crea las tablas de la base de datos con la nomenclatura aplicacion_modelo. Por lo que si nuestra aplicación se llama ocio, la tabla para el modelo Persona será ocio_persona y para el modelo Hobby será ocio_hobby. Para la relación M2M Django genera una tabla intermedia que relaciona los id's de personas con los id's de hobbies. Esta tabla en nuestro caso será ocio_persona_hobbies ya que el campo de la relación ManyToMany se llama hobbies y se encuentra en el modelo Persona.

Teniendo en cuenta que la query va a hacerse sobre el modelo Hobby utilizaremos una consulta SELECT que realice un COUNT sobre la tabla que relaciona personas con hobbies de tal forma que por cada hobby (ocio_hobby.id) contemos el número de filas en los que el id de la persona relacionada se encuentre en la lista de id's de mujeres.

'SELECT COUNT(*) FROM ocio_persona_hobbies WHERE ocio_persona_hobbies.hobby_id = ocio_hobby.id AND ocio_persona_hobbies.persona_id IN (%s)' % lista_ids

Por último prepararemos nuestra queryset() utilizando extra() para incluir nuestro código SQL. Así quedará el código completo:

mujeres_ids = Persona.objects.filter(sexo='f').values_list('id', flat=True)
lista_ids = ','.join(str(id) for id in mujeres_ids)

hobbies = Hobby.objects.extra(
    select = {
        'personas_count': 'SELECT COUNT(*) FROM ocio_persona_hobbies WHERE ocio_persona_hobbies.hobby_id = ocio_hobby.id AND ocio_persona_hobbies.persona_id IN (%s)' % lista_ids
    },
)

Si queremos hacer nuestro código independiente de los nombres de las tablas de la base de datos podemos usar Persona._meta.db_table para obtener el nombre de la tabla usada para el modelo Persona y del mismo modo para el modelo Hobby. Para obtener el nombre de la tabla que relaciona personas y hobbies usaremos Persona._meta.get_field('hobbies').m2m_db_table().

El código independiente de las tablas quedaría de la siguiente manera:

mujeres_ids = Persona.objects.filter(sexo='f').values_list('id', flat=True)
lista_ids = ','.join(str(id) for id in mujeres_ids)

personas_hobbies = Persona._meta.get_field('hobbies').m2m_db_table()

hobbies = Hobby.objects.extra(
    select = {
        'personas_count': 'SELECT COUNT(*) FROM %s WHERE %s.hobby_id = %s.id AND %s.persona_id IN (%s)' % (personas_hobbies, personas_hobbies, Hobby._meta.db_table, personas_hobbies, lista_ids)
    },
)

Publicado por Antonio Melé el Viernes 13 d Agosto d 2010 | 4 comentarios | Categorías: modelos, querysets, trucos, tutorial