Ir para o conteúdo

Sinais

Por vezes, pode ser necessário ouvir um evento de documento ao salvar, ou seja, deseja-se realizar uma ação específica quando algo acontece nos modelos.

O Django, por exemplo, possui esse mecanismo chamado Sinais, que pode ser muito útil para esses casos e para realizar operações extras quando uma ação ocorre no documento.

Outros ORMs adotaram uma abordagem semelhante a essa e uma excelente foi o Ormar, que adotou a abordagem do Django na sua própria implementação.

O Mongoz, sendo como é desenhado, inspirou-se em ambas as abordagens e também suporta o Sinal.

O que são Sinais

Sinais são mecanismos usados para acionar ações específicas quando ocorre um determinado tipo de evento nos modelos do Mongoz.

Da mesma forma que o Django aborda os sinais em termos de registro, o Mongoz faz isso de maneira semelhante.

Sinais padrão

O Mongoz possui receptores padrão para cada documento criado no ecossistema. Eles podem ser usados prontamente a qualquer momento.

Também existem sinais personalizados caso queira algo "extra" além dos padrões fornecidos.

Como usá-los

Os sinais estão dentro de mongoz.core.signals e para importá-los, basta executar:

from mongoz.core.signals import (
    post_delete,
    post_save,
    post_update,
    pre_delete,
    pre_save,
    pre_update,
)

pre_save

O pre_save é usado quando um documento está prestes a ser guardado e é acionado nas funções Document.save() e Document.objects.create.

pre_save(send: Type["Document"], instance: "Document")

post_save

O post_save é usado após o documento já ter sido criado e guardado na base de dados, ou seja, quando uma instância já existe após o save. Esse sinal é acionado nas funções Document.save() e Document.objects.create.

post_save(send: Type["Document"], instance: "Document")

pre_update

O pre_update é usado quando um documento está prestes a receber as atualizações e é acionado nas funções Document.update() e Document.objects.update.

pre_update(send: Type["Document"], instance: "Document")

post_update

O post_update é usado quando um documento já realizou as atualizações e é acionado nas funções Document.update() e Document.objects.update.

post_update(send: Type["Document"], instance: "Document")

pre_delete

O pre_delete é usado quando um documento está prestes a ser excluído e é acionado nas funções Document.delete() e Document.objects.delete.

pre_delete(send: Type["Document"], instance: "Document")

post_delete

O post_update é usado quando um documento já foi excluído e é acionado nas funções Document.delete() e Document.objects.delete.

post_update(send: Type["Document"], instance: "Document")

Receptor

O receptor é a função ou ação que você deseja executar quando um sinal é acionado, noutras palavras, é o que está a escutar um determinado evento.

Vejamos um exemplo. Dado o seguinte documento.

import asyncio

import mongoz

database_uri = "mongodb://localhost:27017"
registry = mongoz.Registry(database_uri)


class User(mongoz.Document):
    name: str = mongoz.String(max_length=255)
    email: str = mongoz.Email(max_length=255)
    is_verified: bool = mongoz.Boolean(default=False)

    class Meta:
        registry = registry
        database = "my_db"

Pode definir um trigger para enviar um e-mail ao utilizador registrado após a criação do registro usando o sinal post_save. A razão para usar o post_save é porque a notificação deve ser enviada após a criação do registro e não antes. Se fosse antes, o pre_save seria o sinal a ser usado.

from mongoz.core.signals import post_save


def send_notification(email: str) -> None:
    """
    Sends a notification to the user
    """
    send_email_confirmation(email)


@post_save(User)
async def after_creation(sender, instance, **kwargs):
    """
    Sends a notification to the user
    """
    send_notification(instance.email)

Como pode ver, o decorador post_save está a apontar para o documento User, ou seja, está "a escutar" eventos nesse mesmo documento.

Isso é chamado de receptor.

Pode usar qualquer um dos sinais padrão disponíveis ou até mesmo criar seu próprio sinal personalizado.

Requisitos

Ao definir a função ou receptor, ela deve atender aos seguintes requisitos:

  • Deve ser um callable.
  • Deve ter o argumento sender como primeiro parâmetro, que corresponde ao documento do objeto de envio.
  • Deve ter o argumento **kwargs como parâmetro, pois cada documento pode mudar a qualquer momento.
  • Deve ser async porque as operações de documento do Mongoz são aguardadas.

Múltiplos receptores

E se você quiser usar o mesmo receptor para vários modelos? Vamos adicionar agora um documento adicional chamado Profile.

import asyncio

import mongoz

database_uri = "mongodb://localhost:27017"
registry = mongoz.Registry(database_uri)


class User(mongoz.Document):
    name: str = mongoz.String(max_length=255)
    email: str = mongoz.Email(max_length=255)
    is_verified: bool = mongoz.Boolean(default=False)

    class Meta:
        registry = registry
        database = "my_db"


class Profile(mongoz.Document):
    profile_type: str = mongoz.String(max_length=255)

    class Meta:
        registry = registry
        database = "my_db"

A maneira de definir o receptor para ambos pode ser facilmente alcançada da seguinte forma:

from mongoz.core.signals import post_save


def send_notification(email: str) -> None:
    """
    Sends a notification to the user
    """
    send_email_confirmation(email)


@post_save([User, Profile])
async def after_creation(sender, instance, **kwargs):
    """
    Sends a notification to the user
    """
    if isinstance(instance, User):
        send_notification(instance.email)
    else:
        # something else for Profile
        ...

Desta forma, pode corresponder e executar qualquer lógica personalizada sem precisar se repetir muito e mantendo seu código limpo e consistente.

Múltiplos receptores para o mesmo documento

E se agora quiser ter mais de um receptor para o mesmo documento? Na prática, colocaria todos num só lugar, mas talvez queira fazer algo completamente diferente e dividi-los em vários.

Pode facilmente fazer isso desta forma:

from mongoz.core.signals import post_save


def push_notification(email: str) -> None:
    # Sends a push notification
    ...


def send_email(email: str) -> None:
    # Sends an email
    ...


@post_save(User)
async def after_creation(sender, instance, **kwargs):
    """
    Sends a notification to the user
    """
    send_email(instance.email)


@post_save(User)
async def do_something_else(sender, instance, **kwargs):
    """
    Sends a notification to the user
    """
    push_notification(instance.email)

Isto garantirá que cada receptor execute a ação definida.

Desconectando receptores

Se deseja desconectar o receptor e impedi-lo de ser executado para um determinado documento, também pode fazer isso de maneira simples.

from mongoz.core.signals import post_save


def send_notification(email: str) -> None:
    """
    Sends a notification to the user
    """
    send_email_confirmation(email)


@post_save(User)
async def after_creation(sender, instance, **kwargs):
    """
    Sends a notification to the user
    """
    send_notification(instance.email)


# Disconnect the given function
User.meta.signals.post_save.disconnect(after_creation)

# Signals are also exposed via instance
user.signals.post_save.disconnect(after_creation)

Sinais Personalizados

Aqui é onde as coisas ficam interessantes. Muitas vezes, você querer ter o seu próprio Sinal e não depender apenas dos padrões fornecidos, e isso é perfeitamente natural e comum.

O Mongoz permite que os sinais personalizados sejam usados de acordo com o seu próprio design.

Vamos continuar com o mesmo exemplo do documento User.

import asyncio

import mongoz

database_uri = "mongodb://localhost:27017"
registry = mongoz.Registry(database_uri)


class User(mongoz.Document):
    name: str = mongoz.String(max_length=255)
    email: str = mongoz.Email(max_length=255)
    is_verified: bool = mongoz.Boolean(default=False)

    class Meta:
        registry = registry
        database = "my_db"

Agora deseja ter um sinal personalizado chamado on_verify especificamente adaptado às necessidades e lógica do seu User.

Para defini-lo, pode simplesmente fazer:

import asyncio

import mongoz

database_uri = "mongodb://localhost:27017"
registry = mongoz.Registry(database_uri)


class User(mongoz.Document):
    name: str = mongoz.String(max_length=255)
    email: str = mongoz.Email(max_length=255)
    is_verified: bool = mongoz.Boolean(default=False)

    class Meta:
        registry = registry
        database = "my_db"


# Create the custom signal
User.meta.signals.on_verify = mongoz.Signal()

Sim, é assim simples. Só precisa adicionar um novo sinal on_verify aos sinais do documento e, a partir de agora, o documento User terá um novo sinal pronto para ser usado.

Warning

Tenha em mente que os sinais são do tipo nível de classe, o que significa que afetarão todas as instâncias derivadas dele. Esteja atento ao criar um sinal personalizado e os seus impactos.

Agora deseja criar uma funcionalidade personalizada para ser ouvida no novo Sinal.

import asyncio

import mongoz

database_uri = "mongodb://localhost:27017"
registry = mongoz.Registry(database_uri)


class User(mongoz.Document):
    name: str = mongoz.String(max_length=255)
    email: str = mongoz.Email(max_length=255)
    is_verified: bool = mongoz.Boolean(default=False)

    class Meta:
        registry = registry
        database = "my_db"


# Create the custom signal
User.meta.signals.on_verify = mongoz.Signal()


# Create the receiver
async def trigger_notifications(sender, instance, **kwargs):
    """
    Sends email and push notification
    """
    send_email(instance.email)
    send_push_notification(instance.email)


# Register the receiver into the new Signal.
User.meta.signals.on_verify.connect(trigger_notifications)

Agora, não apenas criou o novo receptor trigger_notifications, mas também o conectou ao novo sinal on_verify.

Como usá-lo

Agora é hora de usar o sinal numa lógica personalizada, afinal, ele foi criado para garantir que seja personalizado o suficiente para as necessidades da lógica de negócio.

Para simplificação, o exemplo abaixo será uma lógica muito simples.

async def create_user(**kwargs):
    """
    Creates a user
    """
    await User.query.create(**kwargs)


async def is_verified_user(id: str):
    """
    Checks if user is verified and sends notification
    if true.
    """
    user = await User.get_document_by_id(id))

    if user.is_verified:
        # triggers the custom signal
        await User.meta.signals.on_verify.send(sender=User, instance=user)

Como pode ver, o on_verify é acionado apenas se o utilizador estiver verificado e não em nenhum outro lugar.

Desconectar o sinal

O processo de desconectar o sinal é exatamente o mesmo que antes.

async def trigger_notifications(sender, instance, **kwargs):
    """
    Sends email and push notification
    """
    send_email(instance.email)
    send_push_notification(instance.email)


# Disconnect the given function
User.meta.signals.on_verify.disconnect(trigger_notifications)