SQL Databases with SQLModel
Learn to integrate SQL databases with FastAPI using SQLModel for data persistence and CRUD operations.
🎯 What You'll Learn
- •Learn to set up SQLModel with FastAPI
- •Understand database models and table creation
- •Implement CRUD operations with SQL databases
- •Handle database sessions and connections
- •Work with multiple models for security
SQL Databases with SQLModel
🎯 What You'll Learn
- Set up SQLModel with FastAPI for database operations
- Create database models with proper field configurations
- Implement comprehensive CRUD operations (Create, Read, Update, Delete)
- Handle database sessions and connections using dependency injection
- Use multiple models for enhanced security and API design
- Configure database engines and startup events
📚 Theory
SQLModel is a library created by the same author as FastAPI, designed specifically to work seamlessly with FastAPI applications that need SQL databases. It combines the power of SQLAlchemy (for database operations) with Pydantic (for data validation).
Multiple Models Pattern for Security
The official FastAPI approach uses multiple models to enhance security and API design:
1. Base Model with Shared Fields:
class HeroBase(SQLModel):
name: str = Field(index=True)
age: int | None = Field(default=None, index=True)
2. Table Model for Database:
class Hero(HeroBase, table=True):
id: int | None = Field(default=None, primary_key=True)
secret_name: str
3. Public Model for API Responses:
class HeroPublic(HeroBase):
id: int # Always present in responses
4. Create Model for Input Validation:
class HeroCreate(HeroBase):
secret_name: str # Required for creation
5. Update Model for Partial Updates:
class HeroUpdate(SQLModel):
name: str | None = None
age: int | None = None
secret_name: str | None = None
Key Field Configurations:
primary_key=True: Marks the field as the table's primary keyindex=True: Creates a database index for faster queriesdefault=None: Allows the database to auto-generate values (especially for IDs)
2. Database Engine Setup:
from sqlmodel import create_engine
sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"
connect_args = {"check_same_thread": False}
engine = create_engine(sqlite_url, connect_args=connect_args)
3. Database Session Management:
from sqlmodel import Session
from typing import Annotated
from fastapi import Depends
def get_session():
with Session(engine) as session:
yield session
SessionDep = Annotated[Session, Depends(get_session)]
4. Database Table Creation:
def create_db_and_tables():
SQLModel.metadata.create_all(engine)
@app.on_event("startup")
def on_startup():
create_db_and_tables()
CRUD Operations with Multiple Models
Create Operation with Security:
@app.post("/heroes/", response_model=HeroPublic)
def create_hero(hero: HeroCreate, session: SessionDep):
db_hero = Hero.model_validate(hero)
session.add(db_hero)
session.commit()
session.refresh(db_hero)
return db_hero # Returns HeroPublic (no secret_name)
Read Operations with Public Models:
from sqlmodel import select
# Read multiple with pagination - returns public data only
@app.get("/heroes/", response_model=list[HeroPublic])
def read_heroes(
session: SessionDep,
offset: int = 0,
limit: Annotated[int, Query(le=100)] = 100
):
heroes = session.exec(select(Hero).offset(offset).limit(limit)).all()
return heroes # Automatically filtered to HeroPublic
# Read single by ID - no secret data exposed
@app.get("/heroes/{hero_id}", response_model=HeroPublic)
def read_hero(hero_id: int, session: SessionDep):
hero = session.get(Hero, hero_id)
if not hero:
raise HTTPException(status_code=404, detail="Hero not found")
return hero
Update Operation with Partial Updates:
@app.patch("/heroes/{hero_id}", response_model=HeroPublic)
def update_hero(hero_id: int, hero: HeroUpdate, session: SessionDep):
hero_db = session.get(Hero, hero_id)
if not hero_db:
raise HTTPException(status_code=404, detail="Hero not found")
# Only update fields that were actually sent
hero_data = hero.model_dump(exclude_unset=True)
hero_db.sqlmodel_update(hero_data) # SQLModel's update method
session.add(hero_db)
session.commit()
session.refresh(hero_db)
return hero_db
Delete Operation:
@app.delete("/heroes/{hero_id}")
def delete_hero(hero_id: int, session: SessionDep):
hero = session.get(Hero, hero_id)
if not hero:
raise HTTPException(status_code=404, detail="Hero not found")
session.delete(hero)
session.commit()
return {"ok": True}
🔧 Key Concepts
- Multiple Models Pattern: Use different models for different purposes (security, validation)
- Table Models: Classes with
table=Truerepresent database tables (Hero) - Data Models: Classes without
table=Trueare for API contracts (HeroPublic,HeroCreate) - Model Inheritance: Use base classes to share common fields (
HeroBase) - Response Models: Use
response_modelto control what data is returned to clients - Partial Updates: Use
exclude_unset=Trueto update only provided fields - Database Engine: Manages database connections and should be created once
- Sessions: Handle individual transactions and should be injected per request
- Dependency Injection: Use
Depends()to inject database sessions into endpoints - Query Builder: Use
select()for complex database queries - Pagination: Implement with
offsetandlimitparameters
💡 Best Practices
- Multiple Models for Security: Never expose sensitive data like
secret_namein public APIs - Use Response Models: Always specify
response_modelto control API responses - Model Inheritance: Use base models to avoid duplicating field definitions
- Partial Updates: Use
exclude_unset=Truefor PATCH operations to update only provided fields - Primary Key Required: Every table model must have a primary key field
- Session Per Request: Use dependency injection for database sessions to ensure thread safety
- Proper Error Handling: Always check if records exist before operations
- Database Startup: Create tables during application startup with
@app.on_event("startup") - Connection Arguments: Use
check_same_thread=Falsefor SQLite with FastAPI - Query Limits: Always limit query results to prevent performance issues
- Transaction Management: Use
session.commit()to save changes andsession.refresh()to get updated data
🔗 Additional Resources
💡 Hint
SQLModel combines SQLAlchemy's power with Pydantic's validation - use table=True for database models and separate data models for API contracts.
Ready to Practice?
Now that you understand the theory, let's put it into practice with hands-on coding!
Start Interactive Lesson