Database Models
We'll use SQLModel - a library that combines SQLAlchemy (database ORM) with Pydantic (data validation). It's from the same author as FastAPI.
Why SQLModel?
- One class, two uses - Same model for database AND API
- Type hints - Full IDE support
- Async support - Works with async database drivers
- Familiar - If you know SQLAlchemy or Pydantic, you know SQLModel
Our Data Models
We need to store:
- Users - Basic user info
- Preferences - Dietary preferences
- Chat Sessions - Conversation containers
- Chat Messages - Individual messages
Step 1: Database Connection
Create app/db/database.py:
app/db/database.py
from sqlmodel import SQLModel, create_engine
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from app.core.config import settings
# Sync engine (for creating tables)
sync_engine = create_engine(
settings.database_url.replace("postgresql://", "postgresql+psycopg2://"),
echo=settings.debug
)
# Async engine (for queries)
async_engine = create_async_engine(
settings.database_url.replace("postgresql://", "postgresql+asyncpg://"),
echo=settings.debug
)
# Session factory
async_session = sessionmaker(
async_engine,
class_=AsyncSession,
expire_on_commit=False
)
async def init_db():
"""Create all tables."""
SQLModel.metadata.create_all(sync_engine)
async def get_session():
"""Dependency for getting database sessions."""
async with async_session() as session:
yield session
Understanding the Connection Strings
PostgreSQL connection strings have different formats for different drivers:
Original: postgresql://food:food@localhost:5432/food
Sync (psycopg2): postgresql+psycopg2://... # For table creation
Async (asyncpg): postgresql+asyncpg://... # For async queries
Step 2: User Models
Create app/db/models.py:
app/db/models.py
from datetime import datetime
from typing import Optional
from sqlmodel import SQLModel, Field, Relationship
class User(SQLModel, table=True):
"""User account."""
id: Optional[int] = Field(default=None, primary_key=True)
external_id: str = Field(unique=True, index=True) # From frontend
created_at: datetime = Field(default_factory=datetime.utcnow)
# Relationships
preferences: Optional["UserPreferences"] = Relationship(back_populates="user")
sessions: list["ChatSession"] = Relationship(back_populates="user")
class UserPreferences(SQLModel, table=True):
"""User dietary preferences."""
id: Optional[int] = Field(default=None, primary_key=True)
user_id: int = Field(foreign_key="user.id", unique=True)
dietary_type: str = Field(default="vegetarian")
spice_level: str = Field(default="medium")
allergies: str = Field(default="[]") # JSON array as string
health_goals: str = Field(default="[]")
preferred_cuisines: str = Field(default='["south_indian", "north_indian"]')
updated_at: datetime = Field(default_factory=datetime.utcnow)
# Relationship
user: Optional[User] = Relationship(back_populates="preferences")
Field Options
# Primary key (auto-increment)
id: Optional[int] = Field(default=None, primary_key=True)
# Unique constraint with index for fast lookups
external_id: str = Field(unique=True, index=True)
# Foreign key
user_id: int = Field(foreign_key="user.id")
# Default value
spice_level: str = Field(default="medium")
# Default factory (function called for each new row)
created_at: datetime = Field(default_factory=datetime.utcnow)
Step 3: Chat Models
Add to app/db/models.py:
app/db/models.py (continued)
class ChatSession(SQLModel, table=True):
"""A conversation container."""
id: Optional[int] = Field(default=None, primary_key=True)
user_id: int = Field(foreign_key="user.id")
title: str = Field(default="New Chat")
created_at: datetime = Field(default_factory=datetime.utcnow)
# Relationships
user: Optional[User] = Relationship(back_populates="sessions")
messages: list["ChatMessage"] = Relationship(back_populates="session")
class ChatMessage(SQLModel, table=True):
"""A single message in a conversation."""
id: Optional[int] = Field(default=None, primary_key=True)
session_id: int = Field(foreign_key="chatsession.id")
role: str # "user" or "assistant"
content: str
created_at: datetime = Field(default_factory=datetime.utcnow)
# Relationship
session: Optional[ChatSession] = Relationship(back_populates="messages")
Step 4: Food Model (For Reference)
Our food data lives in ChromaDB, not PostgreSQL. But here's what a food record looks like:
# This is stored in ChromaDB, not PostgreSQL
FoodItem = {
"id": "1",
"name": "Masala Dosa",
"description": "A crispy, savory crepe made from fermented rice and lentil batter...",
"cuisine": "south_indian",
"region": "Karnataka",
"meal_type": ["breakfast", "dinner"],
"spice_level": "medium",
"prep_time_minutes": 30,
"ingredients": ["rice", "urad dal", "potato", "onion", "spices"],
"allergens": [],
"is_vegetarian": True,
"is_vegan": False,
"nutrition": {
"calories": 250,
"protein": 6,
"carbs": 40,
"fat": 8
}
}
Step 5: Export Models
Create app/db/__init__.py:
app/db/__init__.py
from .database import init_db, get_session, async_session
from .models import User, UserPreferences, ChatSession, ChatMessage
__all__ = [
"init_db",
"get_session",
"async_session",
"User",
"UserPreferences",
"ChatSession",
"ChatMessage",
]
Using Models in Endpoints
Here's how you'd use these models in an API endpoint:
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlmodel import select
from app.db import get_session, User, UserPreferences
router = APIRouter()
@router.get("/preferences/{user_id}")
async def get_preferences(
user_id: str,
session: AsyncSession = Depends(get_session)
):
# Find user by external ID
statement = select(User).where(User.external_id == user_id)
result = await session.execute(statement)
user = result.scalar_one_or_none()
if not user:
# Create new user with default preferences
user = User(external_id=user_id)
session.add(user)
await session.commit()
await session.refresh(user)
preferences = UserPreferences(user_id=user.id)
session.add(preferences)
await session.commit()
return preferences
return user.preferences
The Depends Pattern
FastAPI's dependency injection:
async def get_current_user(
session: AsyncSession = Depends(get_session)
):
# This function can itself be a dependency
...
@router.get("/me")
async def get_me(
user: User = Depends(get_current_user) # Injected!
):
return user
Database Operations Cheat Sheet
# Create
user = User(external_id="abc123")
session.add(user)
await session.commit()
# Read
statement = select(User).where(User.external_id == "abc123")
result = await session.execute(statement)
user = result.scalar_one_or_none()
# Update
user.preferences.spice_level = "hot"
await session.commit()
# Delete
await session.delete(user)
await session.commit()
# Refresh (get latest from DB)
await session.refresh(user)
Next, let's integrate ChromaDB for our food search functionality.