Usage Patterns
Practical examples for integrating generated ORM code with FastAPI applications.
FastAPI Application Setup
# main.py
from fastapi import FastAPI, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from orm import get_session, init_db
from orm.users.models import User
from orm.users.schemas import UserCreate, UserRead, UserUpdate
from orm.users.crud import UserRepository
app = FastAPI(title="My API")
@app.on_event("startup")
async def startup():
"""Initialize database on startup."""
await init_db()Basic CRUD Endpoints
List with Pagination
@app.get("/users", response_model=list[UserRead])
async def list_users(
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000),
session: AsyncSession = Depends(get_session),
):
"""List all users with pagination."""
return await UserRepository.get_all(session, skip=skip, limit=limit)Get by ID
@app.get("/users/{user_id}", response_model=UserRead)
async def get_user(
user_id: int,
session: AsyncSession = Depends(get_session),
):
"""Get user by ID."""
user = await UserRepository.get_by_id(session, user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return userCreate
@app.post("/users", response_model=UserRead, status_code=201)
async def create_user(
data: UserCreate,
session: AsyncSession = Depends(get_session),
):
"""Create a new user."""
user = User(**data.model_dump())
return await UserRepository.create(session, user)Update
@app.patch("/users/{user_id}", response_model=UserRead)
async def update_user(
user_id: int,
data: UserUpdate,
session: AsyncSession = Depends(get_session),
):
"""Update user by ID."""
user = await UserRepository.update(
session,
user_id,
data.model_dump(exclude_unset=True),
)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return userDelete
@app.delete("/users/{user_id}", status_code=204)
async def delete_user(
user_id: int,
session: AsyncSession = Depends(get_session),
):
"""Delete user by ID."""
deleted = await UserRepository.delete(session, user_id)
if not deleted:
raise HTTPException(status_code=404, detail="User not found")Custom Repository Methods
Extend generated repositories without modifying them:
# orm/users/custom_repository.py
from sqlalchemy import select, or_
from sqlalchemy.ext.asyncio import AsyncSession
from .models import User
from .crud import UserRepository as BaseRepository
class UserRepository(BaseRepository):
"""Extended User repository with custom methods."""
@staticmethod
async def get_by_email(
session: AsyncSession,
email: str,
) -> User | None:
"""Get user by email."""
statement = select(User).where(User.email == email)
result = await session.execute(statement)
return result.scalars().first()
@staticmethod
async def get_active_users(
session: AsyncSession,
limit: int = 100,
) -> list[User]:
"""Get only active users."""
statement = (
select(User)
.where(User.is_active == True)
.limit(limit)
)
result = await session.execute(statement)
return list(result.scalars().all())
@staticmethod
async def search(
session: AsyncSession,
query: str,
) -> list[User]:
"""Search users by email or username."""
statement = select(User).where(
or_(
User.email.ilike(f"%{query}%"),
User.username.ilike(f"%{query}%"),
)
)
result = await session.execute(statement)
return list(result.scalars().all())Filtering and Sorting
from sqlalchemy import select, desc, asc
from orm.products.models import Product
async def get_products_filtered(
session: AsyncSession,
category: str | None = None,
min_price: float | None = None,
max_price: float | None = None,
sort_by: str = "created_at",
sort_order: str = "desc",
skip: int = 0,
limit: int = 100,
) -> list[Product]:
"""Get products with filters and sorting."""
statement = select(Product)
# Apply filters
if category:
statement = statement.where(Product.category == category)
if min_price is not None:
statement = statement.where(Product.price >= min_price)
if max_price is not None:
statement = statement.where(Product.price <= max_price)
# Apply sorting
if hasattr(Product, sort_by):
order_func = desc if sort_order == "desc" else asc
statement = statement.order_by(order_func(getattr(Product, sort_by)))
# Apply pagination
statement = statement.offset(skip).limit(limit)
result = await session.execute(statement)
return list(result.scalars().all())Transactions
from orm import get_session
from orm.orders.models import Order, OrderItem
from orm.products.models import Product
async def create_order_with_items(
session: AsyncSession,
user_id: int,
items: list[dict],
) -> Order:
"""Create order with items in a transaction."""
async with session.begin():
# Create order
order = Order(user_id=user_id, status="pending")
session.add(order)
await session.flush() # Get order.id
total = 0
for item in items:
# Get product and check stock
product = await session.get(Product, item["product_id"])
if not product or product.stock < item["quantity"]:
raise ValueError(f"Insufficient stock for product {item['product_id']}")
# Create order item
order_item = OrderItem(
order_id=order.id,
product_id=product.id,
quantity=item["quantity"],
price=product.price,
)
session.add(order_item)
# Update stock
product.stock -= item["quantity"]
total += product.price * item["quantity"]
order.total = total
# Commit happens automatically at end of `begin()` block
return orderEager Loading Relationships
from sqlalchemy import select
from sqlalchemy.orm import selectinload, joinedload
from orm.users.models import User
async def get_user_with_profile(
session: AsyncSession,
user_id: int,
) -> User | None:
"""Eagerly load user with profile."""
statement = (
select(User)
.options(selectinload(User.profile))
.where(User.id == user_id)
)
result = await session.execute(statement)
return result.scalars().first()
async def get_users_with_posts(
session: AsyncSession,
limit: int = 100,
) -> list[User]:
"""Get users with their posts (avoid N+1)."""
statement = (
select(User)
.options(selectinload(User.posts))
.limit(limit)
)
result = await session.execute(statement)
return list(result.scalars().all())Bulk Operations
from sqlalchemy import update, delete
from orm.users.models import User
async def deactivate_inactive_users(
session: AsyncSession,
days: int = 90,
) -> int:
"""Bulk deactivate users inactive for N days."""
from datetime import datetime, timedelta
cutoff = datetime.utcnow() - timedelta(days=days)
statement = (
update(User)
.where(User.last_login < cutoff)
.where(User.is_active == True)
.values(is_active=False)
)
result = await session.execute(statement)
await session.commit()
return result.rowcount
async def delete_unverified_users(
session: AsyncSession,
) -> int:
"""Bulk delete unverified users."""
statement = (
delete(User)
.where(User.is_verified == False)
)
result = await session.execute(statement)
await session.commit()
return result.rowcountTesting
Test Fixtures
# tests/conftest.py
import pytest
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from sqlmodel import SQLModel
TEST_DATABASE_URL = "sqlite+aiosqlite:///:memory:"
@pytest.fixture
async def engine():
"""Create test database engine."""
engine = create_async_engine(TEST_DATABASE_URL, echo=False)
async with engine.begin() as conn:
await conn.run_sync(SQLModel.metadata.create_all)
yield engine
async with engine.begin() as conn:
await conn.run_sync(SQLModel.metadata.drop_all)
await engine.dispose()
@pytest.fixture
async def session(engine):
"""Create test database session."""
factory = sessionmaker(
engine,
class_=AsyncSession,
expire_on_commit=False,
)
async with factory() as session:
yield sessionRepository Tests
# tests/test_users.py
import pytest
from orm.users.models import User
from orm.users.crud import UserRepository
@pytest.mark.asyncio
async def test_create_user(session):
user = User(
email="[email protected]",
username="testuser",
is_active=True,
)
created = await UserRepository.create(session, user)
assert created.id is not None
assert created.email == "[email protected]"
@pytest.mark.asyncio
async def test_get_user_by_id(session):
# Create
user = User(email="[email protected]", username="testuser")
await UserRepository.create(session, user)
# Get
found = await UserRepository.get_by_id(session, user.id)
assert found is not None
assert found.email == "[email protected]"
@pytest.mark.asyncio
async def test_update_user(session):
user = User(email="[email protected]", username="testuser")
await UserRepository.create(session, user)
updated = await UserRepository.update(
session,
user.id,
{"email": "[email protected]"},
)
assert updated.email == "[email protected]"
@pytest.mark.asyncio
async def test_delete_user(session):
user = User(email="[email protected]", username="testuser")
await UserRepository.create(session, user)
deleted = await UserRepository.delete(session, user.id)
assert deleted is True
assert await UserRepository.get_by_id(session, user.id) is None
@pytest.mark.asyncio
async def test_count_users(session):
for i in range(5):
user = User(email=f"test{i}@example.com", username=f"user{i}")
await UserRepository.create(session, user)
count = await UserRepository.count(session)
assert count == 5Environment Configuration
# .env
DATABASE_URL=postgresql+asyncpg://user:pass@localhost:5432/mydb
# For sync mode
DATABASE_URL=postgresql://user:pass@localhost:5432/mydbProject Structure
Recommended FastAPI project structure with generated ORM:
myproject/
├── django/ # Django project (source of truth)
│ ├── apps/
│ │ ├── users/
│ │ │ └── models.py # Django models
│ │ └── products/
│ │ └── models.py
│ └── manage.py
└── fastapi/ # FastAPI project
└── src/
├── main.py # FastAPI app
├── api/
│ ├── users.py # User endpoints
│ └── products.py # Product endpoints
└── orm/ # Generated ORM (don't edit!)
├── __init__.py
├── database.py
├── users/
└── products/Regeneration Workflow
After Django model changes:
# In Django project
python manage.py generate_fastapi --output-dir=../fastapi/src/orm/
# Or with Makefile
make ormTAGS: fastapi, integration, testing, transactions, queries DEPENDS_ON: [overview, configuration, generated-code]
Last updated on