Software Development

Exploring Events and Event Listeners in SQLAlchemy ORM

In SQLAlchemy, events and event listeners provide a powerful mechanism for executing custom code in response to specific actions or changes within the ORM. These can be particularly useful for scenarios like tracking changes, sending notifications, or enforcing business logic. This blog will delve into the concept of events and event listeners in SQLAlchemy ORM […]
Steve Biko
Exploring Events and Event Listeners in SQLAlchemy ORM

In SQLAlchemy, events and event listeners provide a powerful mechanism for executing custom code in response to specific actions or changes within the ORM. These can be particularly useful for scenarios like tracking changes, sending notifications, or enforcing business logic. This blog will delve into the concept of events and event listeners in SQLAlchemy ORM using a practical example.

Understanding the Code Snippet

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()
from sqlalchemy import Enum as EnumType

class Status(str, Enum):
    PENDING = "pending"
    REJECTED = "rejected"
    APPROVED = "approved"

class FuelRequest(AbstractBaseModel):
    __tablename__ = "fuels"

    id = Column(Integer, primary_key=True, index=True, autoincrement=True)
    public_id = Column(String(50), unique=True)
    status = Column(EnumType(Status), default=Status.PENDING)
    user_id = Column(Integer, ForeignKey("users.id"))
    user = relationship("User", back_populates="fuels")
from sqlalchemy import event
from sqlalchemy.orm.attributes import get_history
from ..utils import get_db

def track_status_changes(mapper, connection, target):
    # Get the history of the 'status' attribute
    hist = get_history(target, 'status')

    if hist.has_changes():
        # Get the user's phone number
        db = next(get_db())
        user_phone_number = get_user_phone_number(db, user_id=target.user_id)

        # Create a message
        message = f"The status of your {target.__class__.__name__} Request has changed to {target.status}."
        # send_notification(user_phone_number, message)
        send_sms_notification(user_phone_number, message)

# Add the event listeners
event.listen(FuelRequest, 'after_update', track_status_changes)

In this example, an event listener named track_status_changes is defined. This listener is triggered after an update operation ('after_update') on instances of the FuelRequest classes. The listener checks if the ‘status’ attribute of the target object has changed. If so, it retrieves the user’s phone number and sends an SMS notification about the status change.

Breaking Down the Code

Defining the Event Listener

def track_status_changes(mapper, connection, target):
    # Get the history of the 'status' attribute
    hist = get_history(target, 'status')

    if hist.has_changes():
        # Get the user's phone number
        db = next(get_db())
        user_phone_number = get_user_phone_number(db, user_id=target.user_id)

        # Create a message to be sent
        message = f"The status of your {target.__class__.__name__} Request has changed to {target.status}."
        # Call the  send_sms_notification to send the message to the user using the users phone number
        send_sms_notification(user_phone_number, message)

The track_status_changes function is the event listener. It takes three

parameters: mapperconnection, and target.

The mapper parameter refers to ORM Mapper for the object.


The connection parameter refers to the database connection in use.


The target parameter refers to the instance of the class (e.g. FuelRequest) that triggered the event.

Inside the listener, it checks if the ‘status’ attribute has changed and, if so, retrieves the user’s phone number and sends an SMS notification.

import os
from twilio.rest import Client
from dotenv import load_dotenv

# Load environment variables from .env file
load_dotenv()

account_sid = os.environ['ACCOUNT_SID']
auth_token = os.environ['AUTH_TOKEN']
phone_number = os.environ['TWILIO_PHONE_NUMBER']
client = Client(account_sid, auth_token)

def get_user_phone_number(db, user_id: int):
    # get the user
    user = db.query(User).filter(User.id == user_id).first()
    # return the user's phone number
    return user.phone_number if user else None

def send_sms_notification(user_phone_number, message_body):
    try:
        message = client.messages.create(
            body=message_body,
            from_= phone_number,  # Your Twilio phone number
            to=user_phone_number  # Get the user phone numbr from the User model
        )
    except Exception as e:
        raise ValueError("Error occurred while sending message: ", e)

Adding Event Listeners

event.listen(FuelRequest, 'after_update', track_status_changes)

The event.listen method is used to attach the track_status_changes listener to the

‘after_update’ event of the specified classes ( FuelRequest). This means that the listener will be

executed every time an instance of these classes undergoes an update operation.

Conclusion

Events and event listeners in SQLAlchemy provide a flexible way to incorporate custom behavior into your ORM-based applications. Whether you need to track changes, enforce business rules, or send notifications, event listeners offer a clean and modular solution. The provided code snippet demonstrates how to use event listeners to respond to updates in the ‘status’ attribute of different ORM entities. Feel free to adapt and extend this example to suit your specific application needs. Happy coding! 🚀💻