Modeling¶
Advanced Alchemy enhances SQLAlchemy’s modeling capabilities with production-ready base classes, mixins, and specialized types. This guide demonstrates modeling for a blog system with posts and tags, showcasing key features and best practices.
Base Classes¶
Advanced Alchemy provides several base classes optimized for different use cases. Any model can utilize these pre-defined declarative bases from sqlchemy. Here’s a brief overview of the included classes:
Base Class |
Features |
---|---|
|
BIGINT primary keys for tables |
|
BIGINT primary keys for tables, Automatic created_at/updated_at timestamps |
|
UUID primary keys |
|
UUIDv6 primary keys |
|
UUIDv7 primary keys |
|
UUID primary keys, Automatic created_at/updated_at timestamps |
|
UUIDv6 primary keys, Automatic created_at/updated_at timestamps |
|
Time-sortable UUIDv7 primary keys, Automatic created_at/updated_at timestamps |
|
URL-friendly unique identifiers, Shorter than UUIDs, collision resistant |
|
URL-friendly IDs with audit timestamps, Combines Nanoid benefits with audit trails |
Mixins¶
Additionally, Advanced Alchemy provides mixins to enhance model functionality:
Mixin |
Features |
---|---|
|
Adds URL-friendly slug field
|
|
Automatic created_at/updated_at timestamps
Tracks record modifications
|
|
Automatic Select or Create for many-to-many relationships
|
Basic Model Example¶
Let’s start with a simple blog post model:
import datetime
from typing import Optional
from advanced_alchemy.base import BigIntAuditBase
from sqlalchemy.orm import Mapped, mapped_column
class Post(BigIntAuditBase):
"""Blog post model with auto-incrementing ID and audit fields.
Attributes:
title: The post title
content: The post content
published: Publication status
created_at: Timestamp of creation (from BigIntAuditBase)
updated_at: Timestamp of last update (from BigIntAuditBase)
"""
title: Mapped[str] = mapped_column(index=True)
content: Mapped[str]
published: Mapped[bool] = mapped_column(default=False)
published_at: Mapped[Optional[datetime.datetime]] = mapped_column(default=None)
import datetime
from advanced_alchemy.base import BigIntAuditBase
from sqlalchemy.orm import Mapped, mapped_column
class Post(BigIntAuditBase):
"""Blog post model with auto-incrementing ID and audit fields.
Attributes:
title: The post title
content: The post content
published: Publication status
created_at: Timestamp of creation (from BigIntAuditBase)
updated_at: Timestamp of last update (from BigIntAuditBase)
"""
title: Mapped[str] = mapped_column(index=True)
content: Mapped[str]
published: Mapped[bool] = mapped_column(default=False)
published_at: Mapped[datetime.datetime | None] = mapped_column(default=None)
Many-to-Many Relationships¶
Let’s implement a tagging system using a many-to-many relationship. This example demonstrates: - Association table configuration - Relationship configuration with lazy loading - Slug key mixin - Index creation
from __future__ import annotations
from sqlalchemy import Column, ForeignKey, Table
from sqlalchemy.orm import relationship
from sqlalchemy.orm import Mapped, mapped_column
from advanced_alchemy.base import BigIntAuditBase, orm_registry, SlugKey
# Association table for post-tag relationship
post_tag = Table(
"post_tag",
orm_registry.metadata,
Column("post_id", ForeignKey("post.id", ondelete="CASCADE"), primary_key=True),
Column("tag_id", ForeignKey("tag.id", ondelete="CASCADE"), primary_key=True)
)
class Post(BigIntAuditBase):
title: Mapped[str] = mapped_column(index=True)
content: Mapped[str]
published: Mapped[bool] = mapped_column(default=False)
# Many-to-many relationship with tags
tags: Mapped[list[Tag]] = relationship(
secondary=post_tag,
back_populates="posts",
lazy="selectin"
)
class Tag(BigIntAuditBase, SlugKey):
"""Tag model with automatic slug generation.
The SlugKey mixin automatically adds a slug field to the model.
"""
name: Mapped[str] = mapped_column(unique=True, index=True)
posts: Mapped[list[Post]] = relationship(
secondary=post_tag,
back_populates="tags",
viewonly=True
)
If we want to interact with the models above, we might use something like the following:
from sqlalchemy.ext.asyncio import AsyncSession
async def add_tags_to_post(
session: AsyncSession,
post: Post,
tag_names: list[str]
) -> Post:
"""Add tags to a post, looking up existing tags and creating new ones if needed."""
existing_tags = await session.scalars(
select(Tag).filter(Tag.slug.in_([slugify(name) for name in tag_names]))
)
new_tags = [Tag(name=name, slug=slugify(name)) for name in tag_names if name not in {tag.name for tag in existing_tags}]
post.tags.extend(new_tags + list(existing_tags))
session.merge(post)
await session.flush()
return post
Fortunately, we can remove some of this logic thanks to UniqueMixin
.
Using UniqueMixin
¶
UniqueMixin
provides automatic handling of unique constraints and merging of duplicate records. When using the mixin,
you must implement two classmethods: unique_hash
and unique_filter
. These methods enable:
Automatic lookup of existing records
Safe merging of duplicates
Atomic get-or-create operations
Configurable uniqueness criteria
Let’s enhance our Tag model with UniqueMixin
:
from advanced_alchemy.base import BigIntAuditBase, SlugKey
from advanced_alchemy.mixins import UniqueMixin
from advanced_alchemy.utils.text import slugify
from sqlalchemy.sql.elements import ColumnElement
from typing import Hashable
class Tag(BigIntAuditBase, SlugKey, UniqueMixin):
"""Tag model with unique name constraint and automatic slug generation.
The UniqueMixin provides:
- Automatic lookup of existing records
- Safe merging of duplicates
- Consistent slug generation
"""
name: Mapped[str] = mapped_column(unique=True, index=True)
posts: Mapped[list[Post]] = relationship(
secondary=post_tag,
back_populates="tags",
viewonly=True
)
@classmethod
def unique_hash(cls, name: str, slug: str | None = None) -> Hashable:
"""Generate a unique hash for deduplication."""
return slugify(name)
@classmethod
def unique_filter(
cls,
name: str,
slug: str | None = None,
) -> ColumnElement[bool]:
"""SQL filter for finding existing records."""
return cls.slug == slugify(name)
from advanced_alchemy.base import BigIntAuditBase, SlugKey
from advanced_alchemy.mixins import UniqueMixin
from advanced_alchemy.utils.text import slugify
from sqlalchemy.sql.elements import ColumnElement
from collections.abc import Hashable
class Tag(BigIntAuditBase, SlugKey, UniqueMixin):
"""Tag model with unique name constraint and automatic slug generation.
The UniqueMixin provides:
- Automatic lookup of existing records
- Safe merging of duplicates
- Consistent slug generation
"""
name: Mapped[str] = mapped_column(unique=True, index=True)
posts: Mapped[list[Post]] = relationship(
secondary=post_tag,
back_populates="tags",
viewonly=True
)
@classmethod
def unique_hash(cls, name: str, slug: str | None = None) -> Hashable:
"""Generate a unique hash for deduplication."""
return slugify(name)
@classmethod
def unique_filter(
cls,
name: str,
slug: str | None = None,
) -> ColumnElement[bool]:
"""SQL filter for finding existing records."""
return cls.slug == slugify(name)
We can now take advantage of UniqueMixin.as_unique_async()
to simplify the logic.
from sqlalchemy.ext.asyncio import AsyncSession
async def add_tags_to_post(
session: AsyncSession,
post: Post,
tag_names: list[str]
) -> Post:
"""Add tags to a post, creating new tags if needed."""
# The UniqueMixin automatically handles:
# 1. Looking up existing tags
# 2. Creating new tags if needed
# 3. Merging duplicates
post.tags = [
await Tag.as_unique_async(session, name=tag_text, slug=slugify(tag_text))
for tag_text in tag_names
]
session.merge(post)
await session.flush()
return post
Customizing Declarative Base¶
In case one of the built in declarative bases do not meet your needs (or you already have your own), Advanced Alchemy already supports customizing the DeclarativeBase
class.
Here’s an example showing a class to generate a server-side UUID primary key for postgres:
import datetime
from uuid import UUID, uuid4
from advanced_alchemy.base import CommonTableAttributes, orm_registry
from sqlalchemy import text
from sqlalchemy.orm import (
DeclarativeBase,
Mapped,
declared_attr,
mapped_column,
orm_insert_sentinel,
)
class ServerSideUUIDPrimaryKey:
"""UUID Primary Key Field Mixin."""
id: Mapped[UUID] = mapped_column(default=uuid4, primary_key=True, server_default=text("gen_random_uuid()"))
"""UUID Primary key column."""
# noinspection PyMethodParameters
@declared_attr
def _sentinel(cls) -> Mapped[int]:
"""Sentinel value required for SQLAlchemy bulk DML with UUIDs."""
return orm_insert_sentinel(name="sa_orm_sentinel")
class ServerSideUUIDBase(ServerSideUUIDPrimaryKey, CommonTableAttributes, DeclarativeBase):
"""Base for all SQLAlchemy declarative models with the custom UUID primary key ."""
registry = orm_registry
# Using ServerSideUUIDBase
class User(ServerSideUUIDBase):
"""User model with ServerSideUUIDBase."""
username: Mapped[str] = mapped_column(unique=True, index=True)
email: Mapped[str] = mapped_column(unique=True)
full_name: Mapped[str]
is_active: Mapped[bool] = mapped_column(default=True)
last_login: Mapped[datetime.datetime | None] = mapped_column(default=None)
With this foundation in place, let’s look at the repository pattern.