Building scalable and maintainable models often requires a modular approach, especially when handling shared behaviors or common column types across multiple models. In this blog, we'll walk through how we can modularize models using SQLAlchemy's mixins and annotations.
Why Modularize?
When working on projects, we frequently encounter repetitive tasks such as adding created_at
and updated_at
timestamps to models or defining common column types like UUID
primary keys. Modularizing these concerns into separate components has several benefits:
1. Reusability: Shared behaviors and column definitions can be used across multiple models.
2. Maintainability: Changes in one place propagate to all dependent models.
3. Readability: Clear separation of concerns makes the code easier to understand.
Creating a Timestamp Mixin
Mixins provide reusable logic or fields for models. Let's define a TimestampMixin that automatically adds created_at
and updated_at
fields to any model that inherits from it.
File: timestamp_mixin.py
from datetime import datetime
from sqlalchemy import Column, DateTime
from sqlalchemy.ext.declarative import declared_attr
class TimestampMixin:
@declared_attr
def created_at(cls):
return Column(DateTime, default=datetime.utcnow, nullable=False)
@declared_attr
def updated_at(cls):
return Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
Explanation
@declared_attr
: Ensures that the attributes are dynamically added to the inheriting models.default
andonupdate
: Automatically set timestamps for creation and updates.
Defining Common Annotations
SQLAlchemy’s Annotated types, available via Python’s typing.Annotated
, let you define reusable column properties. For example, you can define a UUID
primary key or a String
column with specific constraints.
File: common_annotations.py
from typing import Annotated
from uuid import uuid4
from sqlalchemy import String
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import mapped_column
uuid4pk =
mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid4, nullable=False)
]
name = Annotated[
str,
mapped_column(String(30), nullable=False)
]
Explanation
- UUID Primary Key: The
uuid4pk
annotation defines a universally unique identifier for primary keys. - Name Column: The
name
annotation ensures aString
column with a maximum length of 30 characters and noNULL
values.
Building Models with Mixins and Annotations
Using the mixins and annotations, we can define models that inherit shared behavior and properties while keeping the implementation concise and readable.
File: user.py
from sqlalchemy.orm import Mapped
from sqlalchemy.ext.declarative import declarative_base
from mixins import TimestampMixin
from annotations import uuid4pk, name
Base = declarative_base()
class User(Base, TimestampMixin):
__tablename__ = 'users'
id: Mapped[uuid4pk]
first_name: Mapped[name]
last_name: Mapped[name]
Explanation
- Declarative Base: The
Base
serves as the foundation for all SQLAlchemy models.
Creating Models with Mixins and Annotations
Now that we’ve designed our User
model using TimestampMixin
and Annotated
, the next step is to ensure it’s reflected in the database. Upon a successful creation of a table from the model, the columns should look like below:
Benefits of This Approach
1. Clear Separation of Concerns
timestamp_mixin.py
: Contains reusable logic (e.g., timestamps).common_annotations.py
: Defines common column properties (e.g., UUIDs, strings).user.py
: Combines these building blocks into concrete models.
2. Ease of Maintenance
- If it is needed to change how timestamps work or update column constraints, it is only needed to modify the
timestamp_mixin.py
orcommon_annotations.py
files. The changes automatically reflect across all dependent models.
3. Scalability
- As project grows, this structure makes it easier to add new behaviors or field types without introducing redundancy.
Wrap Up Thoughts
Modularizing models with SQLAlchemy's mixins and annotations is a good strategy for handling shared functionality and properties. This approach not only reduces duplication but also aligns with best practices for clean, maintainable code.
Top comments (0)