Signals en Django



@eduardo_gpg

Número de visitas 181

Tiempo de lectura 4 min

4 Marzo 2024

En esta ocasión me gustaría hablemos de un tema sumamente importante al momento de crear aplicaciones con Django, me refiero a los signals.

En términos simples con los signlas seremos capaces de poder ejecutar acciones antes o después de que ocurran ciertos eventos. Esto resulta particular mente útil cuando deseamos ejecutar una serie de acciones relacionadas a la lógica de negocios que, no necesariamente, debe encontrarse directamente en los modelos o las vistas.

Haciendo uso de signals nuestro código será mucho más modular, pudiendo así abstraer y reutilizar funcionalidades.

Veamos un par de ejemplo para que nos quede más en claro. En esta ocasión nos enfocaremos en signlas para nuestros modelos. Si bien es cierto existen signals para las acciones HTTP eso lo dejaremos para una siguiente entrega.

Bien, una vez dicho todo esto, comencemos.

Partamos de los siguientes modelos. El modelo Product y el modelo Price. Una relación bastante sencilla, una relación uno a muchos, donde un producto puede tener muchos precios y un precio le pertenece a un producto.

# products/models.py

class Product(models.Model):
    name = models.CharField(max_length=255)
    description = models.TextField()
    number_of_days = models.PositiveIntegerField(null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.name

class Price(models.Model):
    amount = models.PositiveIntegerField()
    product = models.ForeignKey(Product, 
                                on_delete=models.CASCADE, 
                                related_name='prices')
    created_at = models.DateTimeField(auto_now_add=True)

Con esto en mente, ahora imaginemos que nos encontramos en la necesitas de que, por default, cuando se cree un nuevo producto se establezca su precio en 50 dólares.

Eso se puede hacer de una N cantidad de formas, desde una condicional en las vistas hasta con un manager en los modelos.

Ejemplos.

# products/views.py

product = Product.objects.create(**params)

if product:
    product.prices.create(amount=5000)
# products/models.py

class ProcuctManager(models.Manager):

    def create_with_price(**params):
        product = Product.objects.create(**params)

        if product:
            product.prices.create(amount=5000)

        return product

Inclusive otra opción puede ser la de sobre escribir el método save, algo que en lo particular no recomiendo, pero también es una posible opción.

# products/models.py

class Product(models.Model):
    ...

    def save(self, *args, **kwargs):
        is_new = self._state.adding

        super(Product, self).save(*args, **kwargs)
        if is_new: 
            self.prices.create(amount=5000)

Todos estos approach funcionan, sin embargo ¿Qué pasa si el día de mañana tenemos que añadir más funcionalidades antes o después de crear el producto? quizás ahora notificar vía correo o slack la creación de dicho objeto, comenzar una nueva campaña, dar de baja otros productos, crear precios en Stripe etc..

Con estos nuevos escenarios hacer uso de un Manger, condicionar en las vistas o simplemente sobre escribir ya no suenan a una muy buena idea. Afortunadamente Django nos ofrece los signals, una forma de modularizar nuestro código y ejecutar acciones antes o después que ocurra algún evento en nuestros modelos .

Signals

Django puntualmente nos ofrece un total de 15 signals para nuestros modelos. Estos signals podemos clasificarlos en 6 grupos: Save, Delete, M2M (Many to Many), Migrate, init y Class.

En la gran mayoría de los caso cada grupo tendrán funciones pre y post. Estas funciones, correspondientemente, se ejecutarán antes o después de cierta acción.

Save

  • pre_save
  • post_save

Delete

  • pre_delete
  • post_delete

M2M

  • m2m_changed
  • pre_add
  • post_add
  • pre_remove
  • post_remove
  • pre_clear
  • post_clear

Init

  • pre_init
  • post_init

Migrate

  • pre_migrate
  • post_migrate

Classes Prepared

  • class_prepared: Este signal se dispara cuando una vez el constructor de la clase (Modelo) se ha ejecutado correctamente.

Para nuestro ejemplo anterior, como la relación es uno a muchos y queremos ejecutar una acción después de que se cree un objeto, haremos uso de la función post_save

# products/models.py

from django.dispatch import receiver
from django.db.models.signals import post_save

class Product(models.Model):
    ...

@receiver(post_save, sender=Product)
def create_related_price(sender, instance, created, **kwargs):
    if created:
        Price.objects.create(product=instance, amount=5000)

Aquí hay un par de cosas a destacar. Lo primero que debemos hacer es crear una función donde deseemos ejecutar nuestra lógica de programación. Esta función obligatoriamente debe encontrase decorada por @receiver, que a su vez tendrá el signal y el modelo que disparará dicha señal como argumentos (post_save, sender=Product).

Dependiendo del signas que estemos usando serán los parámetros que nuestra función tendrá. Para conocer con exactitud los parámetros para cada signal te recomiendo ampliamente revises la documentación oficial de Django.

Para nuestro signal post_save tendremos (en el siguiente orden) los parámetros sender (El modelo), instance (el objeto creado o actualizado) y created, parámetro que nos permitirá conocer si el objeto ha sido creado o se ha actualizado.

Si el día de mañana queremos definir nuevas acciones para ejecutarse después de crear nuestro productos basta con definir nuevas funciones decoradas con @receiver y listo.

Letras finales

Cuando hablamos de código siempre hay espacio para la mejora, y el ejemplo anterior no es la excepción.

Si bien nuestro signal funciona, en lo particular no soy partidario de que los signlas y los modelos se encuentren en el mismo archivo, ya que es común que a medida que nuestro proyecto crezca así lo hagan los modelos y la cantidad de signals, pudiendo terminar con archivos de miles de líneas de código.

Lo que recomiendo es lo siguiente.

Creamos un nuevo archivo signals.py en nuestras aplicación.

from .models import Product

@receiver(post_save, sender=Product)
def create_related_price(sender, instance, created, **kwargs):
    if created:
        Price.objects.create(product=instance, amount=5000)

Y en el archivo app.py definimos que, cuando los modelos sean cargados exitosamente se importen, y con ello se registren, los signals.

from django.apps import AppConfig

class ProductsConfig(AppConfig):
    name = 'products'

    def ready(self):
        from . import signals

Y listo, con esto separamos los signals del archivo models py, dejando aun más limpio nuestro código.

Recuerda, siempre que desees ejecutar lógica de negocios antes o después de ciertas acciones en Django te recomiendo ampliamente el uso de signals.


¿El contenido te resulto de ayuda?

Para poder dejar tu opinión es necesario ser un usuario autenticado. Login

Mensana

Por fin una explicación clara!!! Gracias!!

Más Tips y Ejercicios 🐍

Adquiere una subscripción PyWombat por tan solo $3 USD. al mes.

Conoce los beneficios de ser usuario premium:
Niveles desbloqueados: Ten accesos a todos los niveles de ejercicios. 🔓
Nuevo límite: Incrementa tu límite de ejercicios por semana. 🚀
Contenido único: Recibe semanalmente recursos exclusivos de Python (Videos, Artículos y Capitulos del libro PyWombat, comienza como desarrollador Python. 🐍