Django Signals: O guia completo para criar aplicativos responsivos e orientados a eventos

Published on: 2025-10-06
Post image
pt django signals sinais event evento sender receiver

Django Signals forma um mecanismo de eventos interno que permite que partes do sistema fiquem “ouvindo” acontecimentos e reajam de modo desacoplado. Esse recurso habilita um estilo de desenvolvimento orientado a eventos, em que componentes não precisam conhecer uns aos outros diretamente. O resultado é um código mais modular, organizado e fácil de manter em aplicações de qualquer porte. Além disso, o disparo automático de ações melhora a experiência ao tornar respostas mais rápidas e consistentes. Em muitos cenários, sinais substituem gambiarras e reduzem acoplamentos invisíveis.

O funcionamento baseia-se no padrão publicador-assinante, também chamado de “publisher-subscriber”. Quando um evento ocorre, um sinal é emitido e funções registradas como “receptoras” são chamadas. O componente que emite o evento não conhece quem está escutando, o que aumenta a flexibilidade. Receptores podem ser adicionados, alterados ou removidos sem tocar no código que dispara o evento. Esse modelo favorece a extensão de funcionalidades sem fricção.

Arquitetura dos sinais do Django

Um sinal sempre envolve três ideias: quem emite, que evento está acontecendo e quem reage. O emissor é chamado de “sender” e costuma ser um modelo, uma view ou o próprio framework. O evento é o próprio sinal, como post_save ou m2m_changed. O receptor é uma função que recebe parâmetros do evento e executa uma ação de negócio. Essa arquitetura, apesar de simples, preserva baixo acoplamento e alto reuso.

Para ilustrar a anatomia básica, um exemplo com criação de usuário deixa o fluxo claro. O repositório de usuários é emissor, o evento é o salvamento e o receptor reage quando o registro é criado. Variáveis de contexto como a instância salvada chegam ao receptor como argumentos nomeados. A execução é síncrona por padrão, então tarefas pesadas devem ser delegadas a filas assíncronas. O uso de decoradores facilita o registro do receptor no momento da importação.

Os elementos a seguir aparecem em quase todos os sinais e formam a base conceitual do mecanismo:

  • Sender: componente que dispara o evento, como um modelo específico.
  • Signal: tipo de evento, por exemplo pre_save ou post_delete.
  • Receiver: função que processa o evento recebido.
  • Instance: objeto envolvido na ação, geralmente um registro do ORM.

O exemplo a seguir demonstra a estrutura mínima de um receptor de sinal em um salvamento de usuário.

# Anatomia básica de um receptor
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth.models import User as Usuario

@receiver(post_save, sender=Usuario)
def usuario_criado(sender, instance, created, **kwargs):
    # Executa apenas na criação do usuário
    if created:
        print(f"Usuário criado: {instance.username}")  # Log simples

Configuração correta e carregamento de sinais

Para que receptores funcionem, o módulo que os declara precisa ser importado na inicialização do app. O padrão consolidado é criar um arquivo signals.py dentro do aplicativo. Em seguida, o método ready() da configuração do app importa esse módulo. Essa etapa garante o registro dos receptores no ciclo de boot do Django. Sem essa configuração, o sinal pode nunca ser acionado em produção.

O exemplo a seguir mostra uma configuração mínima utilizando AppConfig. O caminho do app precisa corresponder ao pacote real do projeto. O método ready() não deve conter lógica pesada, apenas importações de registro. Essa prática evita lentidão desnecessária na inicialização. A organização mantém os receptores centralizados e previsíveis.

# apps.py
from django.apps import AppConfig

class MinhaAppConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'minhaapp'

    def ready(self):
        # Importa receptores para registrar sinais
        import minhaapp.signals

Sinais de modelo: pre_init e post_init

Os sinais pre_init e post_init envolvem a construção de instâncias de modelos. O primeiro dispara antes da execução do método __init__ e permite ajustar valores padrão. O segundo dispara depois que a instância está pronta, oportuno para armazenar estados iniciais. Esse recurso ajuda a comparar mudanças posteriormente sem consultas extras. Em cenários com validação condicional, o ganho de eficiência é perceptível.

Uma aplicação prática é guardar o valor original de um campo para detecção de alterações. Ao persistir o estado inicial, a lógica de atualização pode agir apenas quando algo mudar. Essa técnica reduz processamento supérfluo e I/O no banco. Em sistemas com muitos campos, a economia acumulada se torna relevante. O trecho a seguir faz esse armazenamento de forma simples.

from django.db.models.signals import post_init
from django.dispatch import receiver
from minhaapp.models import Artigo  # Modelo de exemplo

@receiver(post_init, sender=Artigo)
def guardar_status_inicial(sender, instance, **kwargs):
    # Guarda o status original para comparação em salvamentos
    instance._status_inicial = instance.status

Sinais de modelo: pre_save e post_save

Os sinais pre_save e post_save são os mais usados no ciclo do ORM. O primeiro permite validar e transformar dados antes da gravação, e o segundo registra efeitos colaterais após o commit. Parâmetros como “created” diferenciam criação de atualização, importante para fluxos distintos. O conjunto favorece automações como geração de slug, e-mails de boas-vindas e criação de relacionamentos. Uma abordagem cuidadosa evita recursões acidentais.

Alguns padrões comuns incluem gerar campos derivados, iniciar objetos relacionados e delegar tarefas pesadas. A checagem de campos alterados, quando disponível, evita trabalho redundante. Em cenários de notificação, o envio assíncrono mantém a resposta HTTP rápida. Abaixo, exemplos práticos cobrem casos clássicos de CRUD com eficiência e clareza. Os nomes e comentários seguem convenções em português para facilitar a leitura.

# Geração automática de slug antes de salvar
from django.db.models.signals import pre_save, post_save
from django.dispatch import receiver
from django.utils.text import slugify
from minhaapp.models import PublicacaoBlog, PerfilUsuario, Pedido
from django.core.mail import send_mail

@receiver(pre_save, sender=PublicacaoBlog)
def gerar_slug(sender, instance, **kwargs):
    # Gera slug a partir do título apenas se não existir
    if not instance.slug:
        instance.slug = slugify(instance.titulo)

# Criação de perfil relacionado e e-mail de boas-vindas
from django.contrib.auth.models import User as Usuario

@receiver(post_save, sender=Usuario)
def criar_perfil_e_boas_vindas(sender, instance, created, **kwargs):
    # Na criação, prepara perfil e notifica
    if created:
        PerfilUsuario.objects.create(usuario=instance)
        send_mail(
            subject='Bem-vindo(a)!',
            message=f'Olá, {instance.username}, cadastro concluído.',
            from_email='noreply@exemplo.com',
            recipient_list=[instance.email],
            fail_silently=True
        )

# Processamento condicionado por campos atualizados
from minhaapp.tasks import enviar_confirmacao_pedido  # Tarefa assíncrona

@receiver(post_save, sender=Pedido)
def processar_pedido_concluido(sender, instance, created, **kwargs):
    # Dispara confirmação apenas quando status muda para concluído
    campos = kwargs.get('update_fields')
    if created or campos is None or 'status' in campos:
        if instance.status == 'concluido':
            enviar_confirmacao_pedido.delay(instance.id)

Alguns cuidados práticos aumentam a robustez do uso de salvamentos. Em validações, pre_save evita persistir dados inválidos. Em efeitos colaterais, post_save garante acesso ao registro gravado. A checagem de “created” separa fluxos de criação e atualização. A prevenção de salvamentos recursivos elimina loops indesejados e travamentos.

Sinais de modelo: pre_delete e post_delete

Os sinais pre_delete e post_delete organizam a limpeza de dados e rastros ao apagar registros. O primeiro permite bloquear exclusões ou registrar auditorias antes do ato. O segundo é útil para remover artefatos associados, como arquivos em disco. Essa combinação preserva integridade e economia de recursos. Em ambientes regulados, o registro prévio de exclusão comprova accountability.

Exclusões que envolvem arquivos e dados sensíveis exigem atenção extra. Uma limpeza rigorosa evita órfãos no armazenamento, com benefícios de custo e previsibilidade. Ao mesmo tempo, políticas de proteção podem impedir a remoção de dados críticos. A auditoria de exclusão ajuda a reconstituir eventos e investigar incidentes. Os trechos seguintes cobrem esses três cenários essenciais.

# Limpeza de arquivo e auditoria/controle de exclusão
import os
from django.db.models.signals import pre_delete, post_delete
from django.dispatch import receiver
from django.core.exceptions import PermissionDenied
from django.forms.models import model_to_dict
from minhaapp.models import Documento, Produto, DadosCriticos, RegistroExclusao

@receiver(post_delete, sender=Documento)
def remover_arquivo_fisico(sender, instance, **kwargs):
    # Remove o arquivo em disco após excluir o registro
    if instance.arquivo and os.path.isfile(instance.arquivo.path):
        os.remove(instance.arquivo.path)

@receiver(pre_delete, sender=Produto)
def auditar_exclusao_produto(sender, instance, **kwargs):
    # Cria trilha de auditoria antes da exclusão
    RegistroExclusao.objects.create(
        nome_modelo='Produto',
        objeto_id=instance.id,
        dados=model_to_dict(instance)
    )

@receiver(pre_delete, sender=DadosCriticos)
def impedir_exclusao_protegida(sender, instance, **kwargs):
    # Bloqueia exclusão de dados protegidos
    if instance.protegido:
        raise PermissionDenied('Registro protegido contra exclusão.')

Sinal de relacionamento: m2m_changed

O sinal m2m_changed indica alterações em campos muitos-para-muitos. A ação chega discriminada como adição, remoção ou limpeza, e os pks afetados acompanham o evento. Esse nível de detalhe permite sincronizar permissões, inventários e contadores. Operações em lote com expressões do ORM tornam o processamento eficiente. Em integrações, o momento “post_add” é ideal para disparar notificações.

Reações típicas incluem invalidação de cache, atualizações de contagem e comunicação transacional. O ajuste pontual evita leituras desnecessárias e mantém a consistência vista por quem navega. Em e-learning, a matrícula dispara e-mails e contabiliza vagas. Em comércio, a reserva de itens estabiliza a projeção de disponibilidade. A seguir, exemplos que cobrem esses usos comuns.

# Atualização de permissões e notificações de matrícula
from django.db.models.signals import m2m_changed
from django.dispatch import receiver
from django.core.cache import cache
from django.db.models import F
from django.contrib.auth.models import User as Usuario
from minhaapp.models import Curso, Produto, Pedido, Etiqueta
from minhaapp.tasks import enviar_confirmacao_matricula

@receiver(m2m_changed, sender=Usuario.groups.through)
def invalidar_cache_permissoes(sender, instance, action, pk_set, **kwargs):
    # Limpa cache ao mudar grupos do usuário
    if action in {'post_add', 'post_remove', 'post_clear'}:
        cache.delete(f'permissoes_usuario_{instance.id}')

@receiver(m2m_changed, sender=Curso.alunos.through)
def notificar_matricula(sender, instance, action, pk_set, **kwargs):
    # Envia confirmação após adicionar alunos
    if action == 'post_add':
        for aluno_id in pk_set:
            enviar_confirmacao_matricula.delay(aluno_id, instance.titulo)

@receiver(m2m_changed, sender=Pedido.produtos.through)
def reservar_estoque(sender, instance, action, pk_set, **kwargs):
    # Reserva produto quando vinculado ao pedido
    if action == 'post_add':
        Produto.objects.filter(pk__in=pk_set).update(quantidade_reservada=F('quantidade_reservada') + 1)

@receiver(m2m_changed, sender=Etiqueta.artigos.through)
def atualizar_contador_etiquetas(sender, instance, action, pk_set, **kwargs):
    # Mantém contagem de artigos por etiqueta com operação em lote
    if action == 'post_add':
        Etiqueta.objects.filter(pk__in=pk_set).update(total_artigos=F('total_artigos') + 1)
    elif action == 'post_remove':
        Etiqueta.objects.filter(pk__in=pk_set).update(total_artigos=F('total_artigos') - 1)

Sinal class_prepared: metaprogramação segura

O sinal class_prepared dispara quando uma classe de modelo é registrada pelo Django. Esse instante permite adicionar métodos ou atributos a modelos concretos sem alterar seus códigos-fonte. A prática é útil para padronizar capacidades em vários modelos de uma só vez. Por exemplo, inserir um método utilitário de leitura. O uso deve ser criterioso para não surpreender quem lê o código.

Ao usar metaprogramação, a recomendação é manter as adições simples e documentadas. Métodos com comportamento previsível facilitam testes e manutenção. A checagem de modelos abstratos evita aplicar mudanças onde não se deve. A seguir, demonstra-se a inclusão de um método de nome legível. O ganho é centralizar lógica repetitiva de forma elegante.

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

@receiver(class_prepared)
def incluir_metodo_utilitario(sender, **kwargs):
    # Adiciona método a todo modelo concreto
    if hasattr(sender, '_meta') and not sender._meta.abstract:
        def nome_legivel(self):
            return self._meta.verbose_name
        sender.add_to_class('nome_legivel', nome_legivel)

Sinais do ciclo HTTP: request_started, request_finished e got_request_exception

Sinais de requisição ajudam a medir desempenho e higienizar recursos. request_started indica o início do processamento e request_finished confirma o envio da resposta. O par permite cronometrar duração para identificar gargalos. Já got_request_exception sinaliza exceções durante o atendimento. Com esses pontos, monitoramento e observabilidade tornam-se mais simples.

Uma estratégia eficiente é usar armazenamento local de thread para o cronômetro. Ao fim da resposta, o tempo é comparado e logs são gerados se ultrapassar um limiar. Conexões de banco também podem ser checadas e fechadas quando obsoletas. Em erros, detalhes de URL, método e usuário ajudam a diagnosticar rápido. O exemplo agrega temporização, limpeza e registro de falhas.

# Monitoramento de desempenho e tratamento de exceções
import time
import threading
import logging
from django.core.signals import request_started, request_finished, got_request_exception
from django.dispatch import receiver
from django.db import connection

_logger = logging.getLogger(__name__)
_contexto = threading.local()

@receiver(request_started)
def iniciar_tempo(sender, environ, **kwargs):
    # Marca o início da requisição
    _contexto.inicio = time.time()

@receiver(request_finished)
def finalizar_tempo(sender, **kwargs):
    # Registra requisições lentas e higieniza conexões
    duracao = time.time() - getattr(_contexto, 'inicio', time.time())
    if duracao > 1.0:
        _logger.warning(f'Requisição lenta: {duracao:.2f}s')
    connection.close_if_unusable_or_obsolete()

@receiver(got_request_exception)
def registrar_excecao(sender, request, **kwargs):
    # Loga detalhes essenciais do erro
    _logger.exception(
        'Erro na requisição',
        extra={
            'url': getattr(request, 'path', ''),
            'metodo': getattr(request, 'method', ''),
            'usuario': getattr(request.user, 'username', 'Anonimo')
        }
    )

Sinais de autenticação: user_logged_in, user_logged_out e user_login_failed

O trio de autenticação permite monitorar acessos, sessões e tentativas malsucedidas. user_logged_in confirma login e pode alimentar históricos, mensagens e limites de dispositivos. user_logged_out facilita limpeza de caches e métricas de sessão. user_login_failed ajuda a conter força bruta e informar suspeitas. Combinados, elevam segurança e experiência.

A medição de logins, o limite de sessões por usuário e a notificação de falhas são padrões recorrentes. Ao restringir sessões, a consulta às sessões em banco exige decodificar dados. Em limpeza, múltiplas chaves de cache podem ser invalidadas de uma vez. Em avisos de segurança, a contagem de erros por IP e usuário sustenta bloqueios temporários. Os fragmentos abaixo cobrem essas rotinas.

# Rastreamento de login, limite de sessões e mensagens
from django.contrib.auth.signals import user_logged_in, user_logged_out, user_login_failed
from django.dispatch import receiver
from django.contrib.sessions.models import Session
from django.utils import timezone
from django.core.cache import cache
from django.core.mail import send_mail
from django.contrib import messages
from django.contrib.auth.models import User as Usuario
from minhaapp.models import HistoricoLogin, IPBloqueado

@receiver(user_logged_in)
def registrar_login(sender, request, user, **kwargs):
    # Armazena dados essenciais do login
    HistoricoLogin.objects.create(
        usuario=user,
        ip=request.META.get('REMOTE_ADDR'),
        agente=request.META.get('HTTP_USER_AGENT')
    )
    # Mensagem simples de boas-vindas
    if user.last_login:
        messages.info(request, f'Último acesso: {user.last_login.strftime("%d/%m/%Y %H:%M")}')

@receiver(user_logged_in)
def limitar_sessoes_concorrentes(sender, request, user, **kwargs):
    # Mantém no máximo 3 sessões ativas por usuário
    sessoes_ativas = []
    for sessao in Session.objects.filter(expire_date__gte=timezone.now()):
        dados = sessao.get_decoded()
        if dados.get('_auth_user_id') == str(user.pk):
            sessoes_ativas.append(sessao)
    excesso = len(sessoes_ativas) - 3
    if excesso > 0:
        sessoes_ativas.sort(key=lambda s: s.expire_date)
        for sessao in sessoes_ativas[:excesso]:
            sessao.delete()

@receiver(user_logged_out)
def limpar_cache_usuario(sender, request, user, **kwargs):
    # Remove dados em cache relacionados ao usuário
    if user:
        cache.delete_many([
            f'preferencias_{user.id}',
            f'carrinho_{user.id}',
            f'notificacoes_{user.id}'
        ])

@receiver(user_login_failed)
def proteger_forca_bruta(sender, credentials, request, **kwargs):
    # Contabiliza falhas por IP e usuário e aplica bloqueio
    username = credentials.get('username')
    ip = request.META.get('REMOTE_ADDR') if request else 'desconhecido'
    chave = f'falhas_login_{ip}_{username}'
    tentativas = cache.get(chave, 0) + 1
    cache.set(chave, tentativas, timeout=3600)
    if tentativas >= 5:
        IPBloqueado.objects.get_or_create(endereco=ip, motivo=f'Múltiplas falhas para {username}')
        # Notifica por e-mail quando apropriado
        try:
            usuario = Usuario.objects.get(username=username)
            send_mail(
                subject='Atividade suspeita de login',
                message='Foram detectadas várias tentativas de acesso à conta.',
                from_email='seguranca@exemplo.com',
                recipient_list=[usuario.email],
                fail_silently=True
            )
        except Usuario.DoesNotExist:
            pass

Sinais de migração: pre_migrate e post_migrate

Os sinais pre_migrate e post_migrate organizam tarefas em torno de alterações de esquema. O primeiro pode validar pré-condições, e o segundo inicializa dados padrão. Esse fluxo automatiza configurações após deploy, reduzindo passos manuais. Itens como grupos e permissões são populados com segurança. Além disso, índices de busca e chaves de configuração ficam garantidos.

Ao centralizar inicializações pós-migração, a consistência do ambiente melhora. Equipes passam a confiar que o sistema nasce pronto logo após “migrate”. Verificações de existência evitam duplicações ou falhas em ambientes paralelos. Em integrações, é possível disparar reconstruções de caches e índices. O exemplo a seguir cobre grupos e configurações essenciais.

# Dados padrão e validação após migrações
from django.db.models.signals import post_migrate
from django.dispatch import receiver
from django.contrib.auth.models import Group, Permission
from minhaapp.models import ConfigSistema

@receiver(post_migrate)
def criar_grupos_padrao(sender, **kwargs):
    # Cria grupo "Editores" com permissões básicas
    editores, criado = Group.objects.get_or_create(name='Editores')
    if criado:
        permissoes = Permission.objects.filter(codename__in=['add_artigo', 'change_artigo'])
        editores.permissions.set(permissoes)

@receiver(post_migrate)
def garantir_configuracoes_criticas(sender, **kwargs):
    # Garante chave de configuração essencial
    ConfigSistema.objects.get_or_create(chave='nome_site', defaults={'valor': 'Minha Aplicação'})

Sinais de teste e debug: setting_changed e template_rendered

Durante testes, certos sinais auxiliam inspeção e controle fino. setting_changed indica alteração em configurações, útil para limpar caches afetados. template_rendered registra templates utilizados e contexto em modo de teste. Esses ganchos aceleram diagnóstico de problemas em renderização. Além disso, reduzem falsos positivos por configuração desatualizada.

Ao limpar cache quando uma chave muda, o estado dos testes permanece confiável. O rastreamento de templates confirma que o caminho de renderização é o esperado. Em suites extensas, esse feedback encurta ciclos de correção. A simplicidade das funções receptoras mantém foco na estabilidade. O bloco abaixo apresenta padrões diretos.

# Limpeza de cache e rastreamento de templates em testes
from django.test.signals import setting_changed, template_rendered
from django.dispatch import receiver
from django.core.cache import cache

@receiver(setting_changed)
def limpar_cache_em_mudanca(sender, setting, value, **kwargs):
    # Evita inconsistências após alterar chave de cache
    if setting == 'CACHE_MIDDLEWARE_KEY_PREFIX':
        cache.clear()

@receiver(template_rendered)
def registrar_template_renderizado(sender, template, context, **kwargs):
    # Log simples para depuração
    print(f'Template renderizado: {template.name}')
    print(f'Variáveis no contexto: {list(context.keys())}')

Sinal do banco de dados: connection_created

O sinal connection_created informa quando uma conexão ao banco é estabelecida. Esse momento permite ajustar parâmetros por fornecedor, como fuso horário no PostgreSQL. Também é possível monitorar contagem de conexões ativas em caches de memória. Esse dado orienta alertas para picos e vazamentos. Com isso, a saúde do pool permanece sob controle.

Configurações por conexão tornam o ambiente previsível e padronizado. Em clusters e ambientes híbridos, essa consistência evita surpresas. A instrumentação de contagem complementa dashboards internos. No caso de excesso, alertas propiciam reação antecipada. O exemplo cobre ajuste de sessão e telemetria simples.

# Ajuste por conexão e monitoramento de contagem
from django.db.backends.signals import connection_created
from django.dispatch import receiver
from django.core.cache import cache
import logging

_logger = logging.getLogger(__name__)

@receiver(connection_created)
def configurar_conexao(sender, connection, **kwargs):
    # Define fuso horário na sessão do PostgreSQL
    if connection.vendor == 'postgresql':
        with connection.cursor() as cursor:
            cursor.execute("SET TIME ZONE 'UTC'")

@receiver(connection_created)
def monitorar_conexoes(sender, connection, **kwargs):
    # Incrementa contagem de conexões ativas
    ativas = cache.get('conn_db_ativas', 0) + 1
    cache.set('conn_db_ativas', ativas, timeout=300)
    if ativas > 50:
        _logger.warning(f'Conexões de banco elevadas: {ativas}')

Boas práticas para receptores de sinal

Boas práticas preservam legibilidade e performance em sistemas orientados a eventos. Receptores devem ser curtos e rápidos, delegando tarefas pesadas a filas assíncronas. Condições devem restringir trabalho apenas ao necessário, conforme mudanças reais. Padrões que causam salvamentos recursivos precisam ser evitados com update() ou update_fields. Em operações em massa, desconectar temporariamente sinais previne efeitos colaterais.

Testes dedicados a sinais garantem que efeitos esperados aconteçam sem regressões. A cobertura deve validar criação de relacionados, disparo de tarefas e filtros condicionais. Além disso, medir o tempo de execução de receptores críticos ajuda a localizar gargalos. A auditabilidade é reforçada por logs sucintos nos pontos-chave. Os trechos seguintes exemplificam essas diretrizes.

# Delegar trabalho pesado a tarefas assíncronas
from django.db.models.signals import post_save
from django.dispatch import receiver
from minhaapp.models import Pedido
from minhaapp.tasks import processar_pedido_assincrono

@receiver(post_save, sender=Pedido)
def pos_salvamento_pedido(sender, instance, **kwargs):
    # Apenas agenda processamento; não faz trabalho pesado aqui
    processar_pedido_assincrono.delay(instance.id)

# Evitar recursão: usar update() em vez de save() em relacionamentos
from minhaapp.models import PerfilUsuario
from django.contrib.auth.models import User as Usuario

@receiver(post_save, sender=PerfilUsuario)
def atualizar_usuario_sem_loop(sender, instance, **kwargs):
    # Atualiza campo sem disparar sinais do usuário
    Usuario.objects.filter(pk=instance.usuario.pk).update(ultima_alteracao=timezone.now())

# Desconectar sinais durante operações em massa
from django.db.models.signals import post_save as sinal_pos_save
from minhaapp.models import Comentario

def criar_em_lote_sem_sinal(comentarios):
    from minhaapp.signals import notificar_comentario  # Receptor
    sinal_pos_save.disconnect(notificar_comentario, sender=Comentario)
    try:
        Comentario.objects.bulk_create(comentarios)
    finally:
        sinal_pos_save.connect(notificar_comentario, sender=Comentario)

Estratégias de desempenho com sinais

Algumas estratégias elevam o desempenho quando muitos eventos ocorrem. Processamentos em lote reduzem sobrecarga por item e melhoram throughput. Salvamentos seletivos com update() evitam disparar sinais quando não necessário. Adoção de tarefas assíncronas remove trabalhos longos da linha crítica. Em conjunto, a aplicação responde mais rápido e escala melhor.

Filas temporárias em cache permitem acumular itens e processar por blocos. A decisão do tamanho do lote depende do perfil de carga e do SLA. Em gravações rotineiras, o uso de update() no queryset é suficiente para ajustes simples. Para cargas de CPU ou I/O intensivas, Celery ou soluções equivalentes são a rota indicada. O código seguinte mostra padrões práticos.

# Acúmulo em lote e processamento assíncrono
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.core.cache import cache
from minhaapp.models import Produto
from minhaapp.tasks import atualizar_indice_busca_lote, processamento_pesado

@receiver(post_save, sender=Produto)
def enfileirar_indice_busca(sender, instance, **kwargs):
    # Acumula IDs para processamento em lote
    fila = set(cache.get('fila_indice', []))
    fila.add(instance.id)
    cache.set('fila_indice', list(fila), 300)
    if len(fila) >= 100:
        atualizar_indice_busca_lote.delay(list(fila))
        cache.delete('fila_indice')

# Salvamento sem disparar sinais: use update() no queryset
def atualizar_preco_sem_sinal(produto_id, novo_preco):
    # Atualiza direto no banco
    Produto.objects.filter(pk=produto_id).update(preco=novo_preco)

# Offload de rotina pesada
def agendar_processamento_pesado(objeto_id):
    processamento_pesado.delay(objeto_id)

Exemplo integrado: plataforma de e-commerce

Em um e-commerce, múltiplos sinais colaboram para orquestrar pedidos, estoque e comunicação. O salvamento do pedido dispara e-mail, atualização de inventário e notificação. Alterações em relações muitos-para-muitos recalculam totais de maneira automática. A entrada do usuário restaura itens do carrinho, melhorando continuidade de compra. Antes de excluir produtos, dados são arquivados para análise histórica.

Com esses pontos, a aplicação permanece responsiva e coesa sem acoplamentos desnecessários. Cada receptor é pequeno e delega o que for pesado. A consistência de dados é mantida por contagens e reservas transacionais. O código a seguir ilustra uma configuração típica e funcional. Os nomes foram adaptados para português, preservando clareza e intenção.

# signals.py de um e-commerce
from django.db.models.signals import post_save, pre_delete, m2m_changed
from django.contrib.auth.signals import user_logged_in
from django.dispatch import receiver
from django.contrib.auth.models import User as Usuario
from minhaapp.models import Pedido, Produto, Notificacao, ItemCarrinho, ProdutoArquivado
from minhaapp.tasks import enviar_email_pedido, atualizar_estoque
from django.utils import timezone

@receiver(post_save, sender=Pedido)
def ao_criar_pedido(sender, instance, created, **kwargs):
    # Na criação, dispara efeitos colaterais essenciais
    if created:
        enviar_email_pedido.delay(instance.id)          # E-mail assíncrono
        atualizar_estoque.delay(instance.id)            # Ajuste de inventário
        Notificacao.objects.create(                      # Confirmação ao cliente
            usuario=instance.usuario,
            mensagem=f'Pedido #{instance.id} confirmado'
        )

@receiver(m2m_changed, sender=Pedido.produtos.through)
def recalcular_total_pedido(sender, instance, action, **kwargs):
    # Recalcula total ao mudar itens
    if action in {'post_add', 'post_remove', 'post_clear'}:
        instance.total = sum(p.preco for p in instance.produtos.all())
        instance.save(update_fields=['total'])

@receiver(user_logged_in)
def restaurar_carrinho(sender, request, user, **kwargs):
    # Restaura carrinho na sessão
    itens = ItemCarrinho.objects.filter(usuario=user)
    request.session['carrinho'] = list(itens.values('produto_id', 'quantidade'))

@receiver(pre_delete, sender=Produto)
def arquivar_produto(sender, instance, **kwargs):
    # Preserva dados para análise antes de excluir
    ProdutoArquivado.objects.create(
        original_id=instance.id,
        nome=instance.nome,
        vendas=instance.total_vendas,
        receita=instance.receita_total,
        arquivado_em=timezone.now()
    )

Síntese: onde sinais brilham

Sinais do Django brilham quando ações reativas precisam acontecer sem acoplamento rígido. Automatizações como criação de dependências, limpeza de resíduos e notificações ganham consistência. O impacto em experiência aparece em respostas rápidas, feedback imediato e dados coerentes. A manutenção se beneficia de pontos de extensão claros e testáveis. Em conjunto, o sistema torna-se realmente orientado a eventos.

O uso criterioso evita armadilhas de recursão, processamento pesado na linha crítica e dependências circulares. Adoção de tarefas assíncronas, operações em lote e filtros condicionais mantém o desempenho elevado. A configuração via AppConfig garante que tudo carregue da forma correta. Com boas práticas e observabilidade, sinais sustentam aplicações escaláveis e confiáveis. Esse repertório cobre a base necessária para construir rotinas robustas nesse mecanismo.