arrow_back Volver
Inicio keyboard_arrow_right Artículos keyboard_arrow_right Artículo

Generar y concatenar filtros con Django

Eduardo Ismael Garcia

Full Stack Developer at Código Facilito.

av_timer 5 Min. de lectura

remove_red_eye 28212 visitas

calendar_today 03 Junio 2019

Uno de las principales features de Django es sin duda su ORM. A traves de él seremos capaces de interactuar con la base de datos de una forma muy sencilla y sin la necesidad de dominar el lenguaje de consultas SQL. Para consultas básicas, por ejemplo, trabajar con una o dos condiciones este ORM nos viene perfecto, sin embargo, si nuestras reglas de negocio son complejas así lo serán las 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, difícil de mantener. 😰

Es por ello que en esta ocasión me gustaría que trabajaramos con las clases Manager y Queryset, clases que nos permiten crear y concatenar filtros, de tal forma que podamos generar consultas sumamente complejas, manteniendo, por supuesto, la calidad en nuestros proyectos. 😎

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

Bien, sin más por mencionar, comencemos.

Manager

Para este post estaré trabajando con el siguiente modelo. Un modelo bastante sencillo.

class Video(models.Model):
    title = models.CharField(max_length=50)
    duration = models.IntegerField(default=0) # Segundos
    visible = models.BooleanField(default=False)
    published_at = models.DateTimeField()
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.title

Lo primero que haré será ingresar al shell interactivo de Django, es allí donde ejecutamos nuestras primeras consultas.

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

Comencemos con algo sencillo. Obtengamos todos los vídeos visibles cuya duración sea mayor a cinco minutos (300 segundos). La consulta pudierá quedar de la siguiente manera.

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

Observemos la facilidad con la cual es posible concatenar filtros. Esto se debe, principalmente, ya que el método filter retorna un objeto de tipo Queryset. Ojo aquí 👀

Si has trabajado anteriormente con algún otro ORM muy probablemente te hayas percatado que la consulta no se realiza sobre la clase per se, sino sobre el atributo objects, esto se debe, en esencia, por que el atributo objects representa al modelo. Cualquier operación que implique trabajar con el modelo, ya sea para insertar, actualizar, consultar o eliminar, se deben realizar sobre el atributo objects, y no sobre la clase. Esto nos debe quedar muy en claro, ya que apartir de esto seremos capaces de sacarle el máximo provecho al ORM.

Ahora, que les parece si complicamos un poco más la consulta (ya verán a dónde quiero llegar). Obtengamos todos los vídeos visibles, que hayan sido publicados esta semana, que tengan un duración mayor a un minuto y poseen en su título la palabra 'códigofacilito'. Sí, lo sé, son muchos filtros.

from datetime import timedelta
from django.utils import timezone

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

Video.objects.filter(visible=True).filter(duration__gt=60).filter(title__icontains='códigofacilito').filter(created_at__gte=this_week)

Bien, todo funciona perfectamente. La consulta nos retorna los objetos que cumplen con las condiciones, sin embargo actualmente estamos trabajando en el shell de Django, ¿Qué pasa si ahora tenemos la necesidad de listar el resultado dentro de un template? por ejemplo un HTML. En ese caso tendríamos que realizar la consulta desde una vista. Como la consulta es un poco larga (prácticamente estamos usando cuatro filtros) lo mejor que podemos hacer es definir un nuevo método el cual se encargue de obtener y retornar la información deseada. Este método, por su puesto, debe ser ejecutado por el objeto objects.

Aquí la pregunta interesante es ¿Cómo podemos extender funcionalidades al objeto objects? la respuesta es muy sencilla, mediante la clase Manager. A traves de la clase Manager seremos capaz de definir nuevos métodos para nuestro objeto objects. Veamos un ejemplo.

class VideoManager(models.Manager):

    def get_by_duration(self, duration=0):
        return self.filter(visible=True).filter(duration__gte=duration)

En este caso definimos una nueva clase Manager. Por convención las clases Managers tendrán la siguiente estructura: Nombre de nuestro modelo + Manager. La clase posee el método get_by_duration, método el retornar todos aquellos objetos visible con una duración mayor a x cantidad. Para que podamos utilizar este método, será necesario modificar nuestro modelo, asignando un nuevo valor al atributo objects.

class Video(models.Model):
    title = models.CharField(max_length=50)
    duration = models.IntegerField(default=0)
    visible = models.BooleanField(default=False)
    published_at = models.DateTimeField(auto_now_add=True)
    created_at = models.DateTimeField(auto_now_add=True)

    objects = VideoManager()

    def __str__(self):
        return self.title

El llamado del método es muy sencillo

Video.objects.get_by_duration(300)

Es importante observar como el uso del método filter queda delgado a la clase Manager y que el método, get_by_duration, retorna un objeto de tipo QuerySet, con lo cual es muy sencillo continuar con la concatenación de filtros.

Video.objects.get_by_duration(300).filter(title__icontains='códigofacilito').filter(created_at__gte=this_week)

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 puede ser realizar una consulta por fecha de creación, podemos generar un Manager que filtre por el campo created_at, de esta forma todos los modelos que cuenten con dicho campo serán candidatos de extender el Manager.

Ya para finalizar con la clase Manager, y para fines prácticos, que les parece si creamos una nueva consulta. Obtengamos todos aquellos objetos que en su título se encuentre 'códigofacilito', y por supuesto, sean visibles.

class VideoManager(models.Manager):

    def get_by_duration(self, duration=0):
        return self.filter(visible=True).filter(duration__gte=duration)

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

Hacemos el llamado.

Video.objects.get_by_title('codigofacilito')

QuerySet

Hasta este punto nuestro modelo y nuestro manager pudieran quedar tal y como se encuentran actualmente, el problema recae en que tenemos dos filtros idénticos, tanto el método get_by_duration como get_by_title realizan un filtro sobre al atributo visible. Para solucionar estos debemos abstraer un poco más nuestra forma de pensar.

Cómo mencione anteriormente, somos capaces de contener filtros gracias a los objetos de tipo QuerySet,así que, con esta misma lógica, que les parece si creamos una clase QuerySet, y allí realizamos las consultas.

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

En este caso la clase solo posee un método, ya que estes es el único filtro que se repite.

Para nosotros utilizar el filtro en nuestro Manager tendremos que agregar un nuevo método y por supuesto, hacer uso de él. El método tendrá por nombre get_queryset.

class VideoManager(models.Manager):

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

    def get_by_duration(self, duration=0):
        return self.get_queryset().visible().filter(duration__gte=duration)

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

Dentro de la clase QuerySet colocaremos cada uno de los filtros que necesitemos, esto mediante métodos. Con esto aplicaremos el refrán divide y vencerás. Cada uno de los métodos dentro de la clase QuerySet serán muy sencillo, pero en conjunto nos permitirán realizar consultas sumamente complejas. De igual forma, si en algún momento queremos modificar algún filtro en concreto no será necesario buscar dentro de toda nuestra aplicación, basta con dirigirse al método listo.

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 aún mejor organizado y no caigamos en problemas de lectura.


Bien, y de esta forma es como 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 usaremos en más de un ocasión las clases Manager y Queryset nos serán de gran utilidad.