DEV Community

Atanu Saha
Atanu Saha

Posted on • Edited on

Modularizing SQLAlchemy Models with Mixins and Annotations

Modularizing SQLAlchemy Models
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)
Enter fullscreen mode Exit fullscreen mode
Explanation
  • @declared_attr: Ensures that the attributes are dynamically added to the inheriting models.
  • default and onupdate: 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)
]
Enter fullscreen mode Exit fullscreen mode
Explanation
  • UUID Primary Key: The uuid4pk annotation defines a universally unique identifier for primary keys.
  • Name Column: The name annotation ensures a String column with a maximum length of 30 characters and no NULL 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]
Enter fullscreen mode Exit fullscreen mode
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:
User DB Columns

 

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 or common_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)