Comprehensive SQLAlchemy skill for customer support tech enablement, covering ORM patterns, session management, query optimization, async operations, and PostgreSQL integration
SQLAlchemy is the premier SQL toolkit and Object Relational Mapper (ORM) for Python, providing a comprehensive suite of tools for working with databases. This guide focuses on using SQLAlchemy 2.0+ in customer support systems, covering everything from basic setup to advanced production patterns.
Customer support systems have unique requirements that SQLAlchemy addresses exceptionally well:
# Core SQLAlchemy
pip install sqlalchemy>=2.0
# PostgreSQL driver (async)
pip install asyncpg
# PostgreSQL driver (sync)
pip install psycopg2-binary
# Migration tool
pip install alembic
# Testing
pip install pytest pytest-asyncio
pip install fastapi[all] sqlalchemy[asyncio] asyncpg alembic
pip install flask flask-sqlalchemy sqlalchemy psycopg2-binary alembic
Create a base class and define your customer support models:
from datetime import datetime
from typing import Optional, List
from sqlalchemy import String, Integer, DateTime, Text, ForeignKey, Boolean
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
from sqlalchemy.sql import func
class Base(DeclarativeBase):
"""Base class for all models"""
pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
full_name: Mapped[str] = mapped_column(String(255))
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now()
)
# Relationships
tickets: Mapped[List["Ticket"]] = relationship(
"Ticket",
back_populates="creator",
cascade="all, delete-orphan"
)
class Ticket(Base):
__tablename__ = "tickets"
id: Mapped[int] = mapped_column(primary_key=True)
ticket_number: Mapped[str] = mapped_column(String(50), unique=True, index=True)
title: Mapped[str] = mapped_column(String(500))
description: Mapped[str] = mapped_column(Text)
status: Mapped[str] = mapped_column(String(50), default="open", index=True)
creator_id: Mapped[int] = mapped_column(ForeignKey("users.id"), index=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now()
)
# Relationships
creator: Mapped["User"] = relationship("User", back_populates="tickets")
For async applications (recommended with FastAPI):
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
# Database URL
DATABASE_URL = "postgresql+asyncpg://user:password@localhost:5432/support_db"
# Create async engine
async_engine = create_async_engine(
DATABASE_URL,
pool_pre_ping=True,
echo=True # Set to False in production
)
# Create session factory
AsyncSessionLocal = async_sessionmaker(
bind=async_engine,
class_=AsyncSession,
expire_on_commit=False
)
# Dependency for FastAPI
async def get_db():
async with AsyncSessionLocal() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
async def init_db():
"""Initialize database tables"""
async with async_engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
# Run initialization
import asyncio
asyncio.run(init_db())
Create:
from sqlalchemy import select
async def create_ticket(
session: AsyncSession,
ticket_number: str,
title: str,
description: str,
creator_id: int
) -> Ticket:
"""Create a new support ticket"""
ticket = Ticket(
ticket_number=ticket_number,
title=title,
description=description,
creator_id=creator_id
)
session.add(ticket)
await session.commit()
await session.refresh(ticket)
return ticket
Read:
async def get_ticket_by_number(
session: AsyncSession,
ticket_number: str
) -> Optional[Ticket]:
"""Retrieve ticket by number"""
stmt = (
select(Ticket)
.where(Ticket.ticket_number == ticket_number)
.options(
joinedload(Ticket.creator) # Eager load the creator
)
)
result = await session.execute(stmt)
return result.scalar_one_or_none()
Update:
from sqlalchemy import update
async def update_ticket_status(
session: AsyncSession,
ticket_id: int,
new_status: str
) -> bool:
"""Update ticket status"""
stmt = (
update(Ticket)
.where(Ticket.id == ticket_id)
.values(status=new_status)
)
result = await session.execute(stmt)
await session.commit()
return result.rowcount > 0
Delete:
from sqlalchemy import delete
async def delete_ticket(
session: AsyncSession,
ticket_id: int
) -> bool:
"""Delete a ticket"""
stmt = delete(Ticket).where(Ticket.id == ticket_id)
result = await session.execute(stmt)
await session.commit()
return result.rowcount > 0
SQLAlchemy provides two main ways to work with databases:
Direct SQL construction with Python objects - more explicit, closer to raw SQL:
from sqlalchemy import select, insert, update, delete
# Explicit SQL construction
stmt = (
select(Ticket.id, Ticket.title, User.full_name)
.join(User, Ticket.creator_id == User.id)
.where(Ticket.status == "open")
.order_by(Ticket.created_at.desc())
)
result = await session.execute(stmt)
rows = result.all()
Use when:
Work with Python objects that map to database tables:
from sqlalchemy import select
from sqlalchemy.orm import joinedload
# Object-oriented approach
stmt = (
select(Ticket)
.options(joinedload(Ticket.creator))
.where(Ticket.status == "open")
.order_by(Ticket.created_at.desc())
)
result = await session.execute(stmt)
tickets = result.scalars().all()
# Access as objects
for ticket in tickets:
print(f"{ticket.title} by {ticket.creator.full_name}")
Use when:
Best Practice: Use ORM for business logic and Core for complex analytics queries.
Automatically handle relationships between tickets, users, comments, and attachments:
# Access related objects seamlessly
ticket = await get_ticket(session, ticket_id)
print(f"Creator: {ticket.creator.full_name}")
print(f"Comments: {len(ticket.comments)}")
for comment in ticket.comments:
print(f" - {comment.author.full_name}: {comment.content}")
Load related data efficiently in a single query:
from sqlalchemy.orm import joinedload, selectinload
# Load ticket with all related data
stmt = (
select(Ticket)
.options(
joinedload(Ticket.creator), # One-to-one/many-to-one
selectinload(Ticket.comments).joinedload(Comment.author), # Collections
selectinload(Ticket.attachments)
)
.where(Ticket.id == ticket_id)
)
result = await session.execute(stmt)
ticket = result.unique().scalar_one()
Build complex queries with multiple conditions:
from sqlalchemy import and_, or_, func
stmt = (
select(Ticket)
.where(
and_(
Ticket.status.in_(["open", "in_progress"]),
or_(
Ticket.priority == "urgent",
func.date_part("day", func.now() - Ticket.created_at) > 7
),
Ticket.title.ilike("%payment%")
)
)
)
Generate reports and statistics:
from sqlalchemy import func, case
# Count tickets by status
stmt = (
select(
Ticket.status,
func.count(Ticket.id).label("count")
)
.group_by(Ticket.status)
)
result = await session.execute(stmt)
status_counts = {row[0]: row[1] for row in result}
Ensure data consistency with proper transaction handling:
async def transfer_ticket_ownership(
session: AsyncSession,
ticket_id: int,
new_owner_id: int
):
"""Transfer ticket with automatic rollback on error"""
async with session.begin(): # Automatic commit/rollback
ticket = await session.get(Ticket, ticket_id)
if not ticket:
raise ValueError("Ticket not found")
old_owner_id = ticket.creator_id
ticket.creator_id = new_owner_id
# Create audit log
audit = AuditLog(
action="transfer_ownership",
ticket_id=ticket_id,
old_value=old_owner_id,
new_value=new_owner_id
)
session.add(audit)
# Commits automatically if no exception
Handle concurrent requests efficiently:
async_engine = create_async_engine(
DATABASE_URL,
pool_size=10, # Maintain 10 connections
max_overflow=20, # Allow 20 additional connections
pool_timeout=30, # Wait 30 seconds for connection
pool_recycle=3600, # Recycle connections after 1 hour
pool_pre_ping=True # Verify connection health
)
# Good
class User(Base):
id: Mapped[int] = mapped_column(primary_key=True)
email: Mapped[str] = mapped_column(String(255))
tickets: Mapped[List["Ticket"]] = relationship()
# Avoid (old style)
class User(Base):
id = Column(Integer, primary_key=True)
email = Column(String(255))
# Good - Single query
tickets = await session.execute(
select(Ticket).options(joinedload(Ticket.creator))
)
# Avoid - N+1 queries
tickets = await session.execute(select(Ticket))
for ticket in tickets.scalars():
print(ticket.creator.name) # Separate query for each ticket!
# Good - Using FastAPI dependency
@app.get("/tickets/{ticket_id}")
async def get_ticket(
ticket_id: int,
session: AsyncSession = Depends(get_db)
):
ticket = await session.get(Ticket, ticket_id)
return ticket
# Avoid - Manual session management in routes
@app.get("/tickets/{ticket_id}")
async def get_ticket(ticket_id: int):
session = AsyncSessionLocal()
ticket = await session.get(Ticket, ticket_id)
# Forgot to close session!
return ticket
class Ticket(Base):
__tablename__ = "tickets"
# Index frequently queried columns
ticket_number: Mapped[str] = mapped_column(String(50), unique=True, index=True)
status: Mapped[str] = mapped_column(String(50), index=True)
creator_id: Mapped[int] = mapped_column(ForeignKey("users.id"), index=True)
class Ticket(Base):
deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
@hybrid_property
def is_deleted(self) -> bool:
return self.deleted_at is not None
# Filter out soft-deleted records
stmt = select(Ticket).where(Ticket.deleted_at.is_(None))
Problem:
# This creates N+1 queries (1 for tickets + N for each creator)
tickets = await session.execute(select(Ticket))
for ticket in tickets.scalars():
print(ticket.creator.full_name) # Lazy load triggers new query!
Solution:
# Single query with eager loading
tickets = await session.execute(
select(Ticket).options(joinedload(Ticket.creator))
)
for ticket in tickets.unique().scalars():
print(ticket.creator.full_name) # Already loaded!
Problem:
async def get_tickets():
session = AsyncSessionLocal()
tickets = await session.execute(select(Ticket))
return tickets.scalars().all()
# Session never closed - connection leak!
Solution:
async def get_tickets():
async with AsyncSessionLocal() as session:
tickets = await session.execute(select(Ticket))
return tickets.scalars().all()
# Session automatically closed
Problem:
engine = create_async_engine(DATABASE_URL)
# Stale connections will cause errors!
Solution:
engine = create_async_engine(
DATABASE_URL,
pool_pre_ping=True # Verify connections are alive
)
Problem:
# NEVER mix sync and async code!
async def bad_example():
session = Session(engine) # Sync session in async function!
# This will cause problems
Solution:
# Use async throughout
async def good_example():
async with AsyncSessionLocal() as session:
# All async operations
result = await session.execute(select(Ticket))
Problem:
# Multiple operations without transaction
ticket = await session.get(Ticket, ticket_id)
ticket.status = "resolved"
await session.commit() # Partial commit
audit = AuditLog(ticket_id=ticket_id, action="resolved")
session.add(audit)
await session.commit() # If this fails, ticket is still resolved!
Solution:
# Atomic transaction
async with session.begin():
ticket = await session.get(Ticket, ticket_id)
ticket.status = "resolved"
audit = AuditLog(ticket_id=ticket_id, action="resolved")
session.add(audit)
# Both committed together or both rolled back
Error: connection pool exceeded
Solution:
# Increase pool size or check for connection leaks
engine = create_async_engine(
DATABASE_URL,
pool_size=20, # Increase from default 5
max_overflow=40 # Increase from default 10
)
Error: server closed the connection unexpectedly
Solution:
# Enable pool_pre_ping
engine = create_async_engine(
DATABASE_URL,
pool_pre_ping=True,
pool_recycle=3600 # Recycle connections every hour
)
Error: greenlet_spawn has not been called
Solution:
# Always eager load relationships in async code
stmt = select(Ticket).options(
joinedload(Ticket.creator),
selectinload(Ticket.comments)
)
Error: Instance is not bound to a Session
Solution:
# Use expire_on_commit=False or refresh objects
SessionLocal = async_sessionmaker(
bind=engine,
expire_on_commit=False # Keep objects usable after commit
)
# Or refresh explicitly
await session.refresh(ticket)
Error: Alembic can't detect changes
Solution:
# Ensure alembic.ini is configured correctly
# Check env.py imports all models
# Force autogenerate
alembic revision --autogenerate -m "migration" --head head
Use EXPLAIN ANALYZE: Understand query execution
from sqlalchemy import text
result = await session.execute(text("EXPLAIN ANALYZE SELECT * FROM tickets"))
Add Indexes: Index foreign keys and filter columns
Use Bulk Operations: For large datasets
Implement Caching: Cache frequently accessed data
Use Read Replicas: Separate read and write operations
Monitor Queries: Log slow queries in production
Optimize Joins: Use appropriate join strategies
Partition Large Tables: By date or other criteria
Use Materialized Views: For complex analytics
Enable Query Logging: During development only
Built with expertise for customer support tech enablement teams.
npx skills add manutej/SQLAlchemy ORM Expert下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer
Tags:python, sqlalchemy, orm, postgresql, database, customer-support, backend, fastapi, async, testing, data-curation