Para quem já trabalhou com Django, sabe dos ótimos recursos disponíveis dentro do framework, além do ilustríssimo e ótimo ORM, uma estrutura de rotas, comunidade enorme e uma vasta experiência para fornecer ferramentas de qualidade para resolução de problemas no desenvolvimento de aplicações web. Mas além desses recursos ele conta também com um o sinais ou signals um disparador de eventos que acontece antes ou depois que uma ação é feita.
Uma das formas mais comuns de usar sinais no Django é quando precisamos executar uma determinada tarefa depois de salvar as informações no banco de dados, claro, partindo de um modelo (model) qualquer. O código seria assim:
from django.db.models.signals import pre_save
from django.core.mail import send_mail
from django.dispatch import receiver
from bank.models import Account
@receiver(post_save, sender=Account)
def send_mail(sender, instance, **kwargs):
if instance.balance < 0:
send_mail(
subject="Saldo baixo!",
message="Você entrou no cheque especial",
from_email="admin@domain.tld",
recipient_list=["user@domain.tld"]
)
Essa seria uma possível implementação, porém falha, dado que parte da instrução da função send_email depende dos dados do modelo, como é visto logo abaixo da definição dela. O problema é que logo após o sinal ser acionado, o banco de dados ainda está processando o salvamento das informações, por tanto pode ser que no momento da consulta do instance.balance o valor seja o anterior e não 0, como seria esperado, isso é conhecido também como race condition.
A resolução do problema é simples e utilizando os próprios recursos do Django. Dado que as duas atividades estão perdendo a comunicação ao longo de suas execuções e uma depende da outra. É necessário esperar uma ser concluída para em seguida acionar a outra. Para isso é necessário garantir que os dados foram salvos para na sequência fazer o disparo da tarefa (send_mail). Para isso podemos usar o modulo de transaction e a função on_commit que é a responsável por garatir que o sinal será chamado ao fim da "transação" de dados no banco (ou seja depois que ele salvar os dados).
O uso do on_commit para essa finalidade teria uma implementação assim:
[...]
from django.db import transaction
def do_mail(instance):
if instance.balance < 0:
send_mail(
subject="Saldo baixo!",
message="Você entrou no cheque especial",
from_email="admin@domain.tld",
recipient_list=["user@domain.tld"]
)
@receiver(post_save, sender=Account)
def send_mail(sender, instance, **kwargs):
transaction.on_commit(lambda: do_email(instance))
Os sinais do Django é uma ferramenta muito útil em muitos casos, mesmo tendo possibilidade de adicionar ferramentas externas para atividades de baixa escala o uso desse recurso se faz necessário, porém é importante entender alguns detalhes para extrair o máximo possível de funcionalidade durante o desenvolvimento de aplicações web.