notifications Notificaciones

Marcar todas como leídas

Ver más

lightbulb_outline

708 veces ha sido leído este artículo

Generar y concatenar filtros con Django

Lo lees en 4 Min.

Una de las principales características de Django es su ORM, con este podemos interactuar con la base de datos de una forma muy sencilla sin la necesidad de dominar el lenguaje de consultas SQL. Para consultas sencillas, por ejemplo, trabajar con una o dos condiciones, este ORM es perfecto, sin embargo, si nuestras reglas de negocio son complejas así lo serán nuestras consultas. Trabajar con consultas complejas utilizando Django no es tarea fácil. Si no tenemos un buen nivel de abstracción podemos terminar con código repetido, difícil de comprender y sobre todo mantener 😰

Es por ello que en esta ocasión me gustaría hablar sobre las clases Manager y Queryset, clases que nos permiten crear y concatenar filtros, de tal forma que podamos generar consultas complejas manteniendo la calidad en nuestro proyecto. 😎

Para este post estaré trabajando con la versión de Django 2.2

Partamos del siguiente modelo.

class Video(models.Model):
    title = models.CharField(max_length=50, blank=False, null=False)
    description = models.TextField()
    visible = models.BooleanField(default=True)
    duration = models.IntegerField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.title

Trabajaremos con la consola de Django y generaremos un par de consultas

python manage.py shell
>>> from videos.models import Video

Comencemos con una consulta sencilla, obtengamos todos los vídeos visibles cuya duración sea mayor a cinco minutos (300 segundos).

Video.objects.filter(visible=True).filter(duration__gt=300)

Si has trabajando con otros ORMs, quizás, te hayas dado cuenta que el filtro no se realiza sobre la clase principal, en mi caso Video, si no sobre el atributo objects. En esencia, objects representa nuestro modelo. Consultar, actualizar, eliminar y crear deben hacerse sobre dicho objeto. Esto es algo de suma importancia que debemos tener muy en cuenta. 😃

Observemos la facilidad con la cual podemos concatenar filtros. Esto se debe principalmente a que el método filter retorna un objeto de tipo Queryset.

Sigamos con las consultas, ahora obtengamos todos los vídeos visibles de esta semana.

from datetime import timedelta
from django.utils import timezone

this_week = timezone.now() - timedelta(days=7)

Video.objects.filter(visible=True).filter(created_at__gte=this_week)

Bien, hasta aquí nada nuevo, compliquemos un poco más las consultas (ya verán a dónde quiero llegar). Obtengamos todos los vídeos visibles, que hayan sido publicados esta semana, su duración sea mayor a un minuto y cuyo título contenga 'codigofacilito'.

video =  Video.objects.filter(visible=True).filter(duration__gt=60).filter(title__icontains='codigofacilito').filter(created_at__gte=this_week)

La consulta funciona, aunque como podemos observar esta se vuelve más compleja. En la consola esto no conlleva muchos problemas, pero, si lo trasladamos a una vista, ya sea mediante una función o una clase, nuestro código puede llegar a complicarse, algo que no es para nada bueno 😓. Lo que podemos hacer es separar los datos, la consulta, de nuestra vista. Para ello regresamos a nuestro archivo models y generamos una nueva clase, clase la cual debe heredar de models.Manager.

La clase Manager no es más que una interfaz que interactúa con la base de datos y nos permite generar nuestros propios queries.

En mi caso la clase pudiese quedar se la siguiente manera.

from datetime import timedelta
from django.utils import timezone

class VideoManager(models.Manager):

    def by_gt_duration(self, duration=0):
        return self.filter(visible=True).filter(duration__gt=300)

    def this_week(self):
        this_week = timezone.now() - timedelta(days=7)
        return self.filter(visible=True).filter(created_at__gte=this_week)

    def by_title_duration_this_week(self, title, duration):
        this_week = timezone.now() - timedelta(days=7)
        return self.filter(visible=True).filter(duration__gt=duration).filter(title__icontains=title).filter(created_at__gte=this_week)

En este caso genero tres nuevos métodos para los tres casos previamente descritos: filtro por duración, filtro por fecha y filtro por titulo, duración y fecha.

Es importante observar como el uso del método filter queda delgado a la clase Manager y que los métodos de esta clase retornan objectos QuerySet, estos nos pemitirá concatenar nuevos filtros u ordenamientos.

Para que podamos implementar los filtros en nuestro modelo basta con extender el objeto objects

class Video(models.Model):
    ...

    objects = VideoManager()

    def __str__(self):
        return self.title

Listo, una vez hecho esto ya podemos ejecutar las consultas.

>>> Video.objects.by_gt_duration(300)
>>> Video.objects.this_week()
>>> Video.objects.by_title_duration_this_week('Harry Potter', 300)

Cool, ahora las consultas complejas se encuentran en nuestra clase Manager y no en nuestra vista. 😎

Lo interesante de utilizar la clase Manager es que no estamos limitados en extender a un solo modelo, si así lo deseamos podemos extender Manager a todos los modelos que deseamos. Por ejemplo, si tenemos una tarea muy común, cómo lo es realizar filtros por fecha de creación, podemos genear un Manager que filtre por el campo created_at, de esta forma todos los modelos que cuenten con dicho campo serán candidatos de extener el Manager.


Hasta este pundo nuestro modelo y nuestro manager puedisen quedar de esta forma, sin embargo, dentro de los métodos encontramos filtros duplicados, principlamente el filtro visible = True y el filtro por fechas. Lo que haremos ahora será abstraer un poco más.

Generamos una nueva clase la cual debe heredar de QuerySet.

class VideoQuery(models.QuerySet):
    def visible(self):
        return self.filter(visible=True)

    def this_week(self):
        this_week = timezone.now() - timedelta(days=7)
        return self.filter(created_at__gte=this_week)

    def duration_gt(self, duration):
        return self.filter(duration__gt=duration)

    def title(self, title):
        return self.filter(title__icontains=title)

Dentro de la clase generemos un nuevo método por cada uno de los filtros que usaremos. En mi caso cuatro, de esta forma podremos generar consultas complejas aplicando el refran divide y venseras.

Si en algún momento necesitamos modificar un filtro en concreto no será necesario buscar dentro de toda nuestra aplicación, basta con dirigirse al método del filtro y listo.

Una vez definidos los nuevos filtro debemos de modificar nuestra clase Manager.

class VideoManager(models.Manager):

    def get_queryset(self):
        return VideoQuery(self.model, using=self._db)  

    def by_gt_duration(self, duration=0):
        return self.get_queryset().visible().duration_gt(duration)

    def this_week(self):
        return self.get_queryset().visible().this_week()

    def by_title_duration_this_week(self, title, duration):
        return self.get_queryset().visible().duration_gt(duration).title(title).this_week()

En este caso a través del método get_queryset podemos utilizar y concatenar los filtros previamente creados.

Si ejecutamos, obtendremos el mismo resultado.

>>> Video.objects.by_gt_duration(300)
>>> Video.objects.this_week()
>>> Video.objects.by_title_duration_this_week('Harry Potter', 300)

De hecho podemos mejorar aún más nuestro código. 🤔 podemos abstraer un poco más el filtro visible y duración.

class VideoQuery(models.QuerySet):
    ...

    def visible_and_duration(self, duration):
        return self.filter(visible=True).filter(duration__gt=duration)

Bueno ya entendieron mi punto. 😅

Algo a destacar es que ahora los filtros de la clase QuerySet pueden ser utilizados en objects. Por ejemplo podemos obtener los videos de esta semana sin importar el titulo, si es o no visible, duración etc...

Video.objects.this_week()

Podemos concatenar con nuevos filtros u ordenamientos.

Video.objects.this_week().order_by('-id')

Ya para finalizar algo que me gustaría mencionar es que es una muy buena idea separar las Manager y QuerySet de nuestros Modelos, esto con la finalidad de mantener nuestro proyecto mejor organizado y no caigamos nuevamente en problemas de lectura.


Bien, y de esta forma es como nosotros podemos generar nuestros propios filtros y concatenarlos, esto a través de abstracciones. Siempre que tengas la necesidad de generar consultas complejas y filtros que usarás en más de un ocasión las clases Manager y Queryset te construir aplicacione escalables y reutilizable, todo con las mejores prácticas.

Recomendaciones

Comunidad