FastAPI Database Models
Database models are the backbone of your FastAPI application's persistence layer. They define how your data is structured in the database and how it can be interacted with in your code. In this tutorial, we'll explore how to create and work with database models in FastAPI using SQLAlchemy ORM (Object-Relational Mapping).
Introduction to Database Models
Database models represent tables in your database as Python classes, and rows as instances of those classes. This abstraction allows you to interact with your database using Python code rather than writing raw SQL queries.
With FastAPI, we typically use SQLAlchemy as the ORM of choice due to its robustness and seamless integration with FastAPI's async features.
Prerequisites
Before diving into database models, you should have:
- Python 3.7+ installed
- Basic knowledge of FastAPI
- Basic understanding of databases
- FastAPI and SQLAlchemy installed:
pip install fastapi sqlalchemy
Setting Up the Database Connection
First, let's create a database configuration file (database.py
) to establish our database connection:
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
# Create a SQLite database URL
SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db"
# For PostgreSQL you'd use: postgresql://user:password@postgresserver/db
# Create the SQLAlchemy engine
engine = create_engine(
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
# The connect_args is needed only for SQLite. For other databases, it's not required.
# Create a SessionLocal class
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# Create a Base class
Base = declarative_base()
# Dependency to get DB session
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
Creating Database Models
Now, let's create some database models. We'll create a sample application for managing blog posts and users.
Create a new file called models.py
:
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, Text, DateTime
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from .database import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True)
username = Column(String, unique=True, index=True)
hashed_password = Column(String)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
posts = relationship("Post", back_populates="author")
class Post(Base):
__tablename__ = "posts"
id = Column(Integer, primary_key=True, index=True)
title = Column(String, index=True)
content = Column(Text)
published = Column(Boolean, default=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
author_id = Column(Integer, ForeignKey("users.id"))
author = relationship("User", back_populates="posts")
comments = relationship("Comment", back_populates="post", cascade="all, delete-orphan")
class Comment(Base):
__tablename__ = "comments"
id = Column(Integer, primary_key=True, index=True)
text = Column(Text)
created_at = Column(DateTime(timezone=True), server_default=func.now())
post_id = Column(Integer, ForeignKey("posts.id"))
user_id = Column(Integer, ForeignKey("users.id"))
post = relationship("Post", back_populates="comments")
user = relationship("User")
Let's break down what we've done:
-
Table Definition: The
__tablename__
attribute specifies the name of the table in the database. -
Columns: We define columns using the
Column
class, specifying the data type and constraints:Integer
,String
,Text
,Boolean
,DateTime
are data typesprimary_key=True
marks a column as the primary keyindex=True
creates an index for faster queriesunique=True
ensures column values are uniqueForeignKey
establishes relationships between tables
-
Relationships: The
relationship
function creates relationships between tables:back_populates
links relationships between modelscascade
specifies how operations should propagate to related records
Creating the Database Tables
Now, we need to create these tables in our database. In your main application file:
from fastapi import FastAPI
from .database import engine
from . import models
# Create tables
models.Base.metadata.create_all(bind=engine)
app = FastAPI()
# Your API endpoints will go here
Pydantic Models for Data Validation
While SQLAlchemy models define the database structure, Pydantic models define the structure of the data that is sent to and from the API. Let's create a schemas.py
file:
from typing import List, Optional
from datetime import datetime
from pydantic import BaseModel, EmailStr
# User schemas
class UserBase(BaseModel):
email: EmailStr
username: str
class UserCreate(UserBase):
password: str
class User(UserBase):
id: int
is_active: bool
created_at: datetime
class Config:
orm_mode = True
# Post schemas
class PostBase(BaseModel):
title: str
content: str
published: bool = False
class PostCreate(PostBase):
pass
class Post(PostBase):
id: int
created_at: datetime
updated_at: Optional[datetime] = None
author_id: int
class Config:
orm_mode = True
class PostWithUser(Post):
author: User
class Config:
orm_mode = True
# Comment schemas
class CommentBase(BaseModel):
text: str
class CommentCreate(CommentBase):
pass
class Comment(CommentBase):
id: int
created_at: datetime
post_id: int
user_id: int
class Config:
orm_mode = True
Notice the Config
class with orm_mode = True
. This allows Pydantic to work with ORM objects, converting them to dictionaries automatically.
Using Models in API Endpoints
Now, let's create some API endpoints that use our models:
from fastapi import Depends, FastAPI, HTTPException, status
from sqlalchemy.orm import Session
from typing import List
from . import models, schemas
from .database import get_db
app = FastAPI()
# Create a user
@app.post("/users/", response_model=schemas.User, status_code=status.HTTP_201_CREATED)
def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)):
# Check if user with this email already exists
db_user_email = db.query(models.User).filter(models.User.email == user.email).first()
if db_user_email:
raise HTTPException(status_code=400, detail="Email already registered")
# Check if username is taken
db_user_name = db.query(models.User).filter(models.User.username == user.username).first()
if db_user_name:
raise HTTPException(status_code=400, detail="Username already taken")
# Create a hashed password (in a real app, use proper hashing)
fake_hashed_password = user.password + "_hashedsecurely"
# Create a new User object
db_user = models.User(
email=user.email,
username=user.username,
hashed_password=fake_hashed_password
)
# Add to database, commit changes, and refresh to get generated id
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
# Get all users
@app.get("/users/", response_model=List[schemas.User])
def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
users = db.query(models.User).offset(skip).limit(limit).all()
return users
# Create a post for a user
@app.post("/users/{user_id}/posts/", response_model=schemas.Post, status_code=status.HTTP_201_CREATED)
def create_post(
user_id: int,
post: schemas.PostCreate,
db: Session = Depends(get_db)
):
# Check if user exists
user = db.query(models.User).filter(models.User.id == user_id).first()
if user is None:
raise HTTPException(status_code=404, detail="User not found")
# Create new post with user_id
db_post = models.Post(**post.dict(), author_id=user_id)
db.add(db_post)
db.commit()
db.refresh(db_post)
return db_post
# Get all posts
@app.get("/posts/", response_model=List[schemas.PostWithUser])
def read_posts(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
posts = db.query(models.Post).offset(skip).limit(limit).all()
return posts
In the example above:
- We use
Depends(get_db)
to inject a database session into our endpoint functions. - We use SQLAlchemy queries to interact with our database models.
- We use Pydantic schemas for request validation and response modeling.
Database Model Relationships
Our examples demonstrate several types of relationships:
-
One-to-Many: A User has many Posts.
python# In User model
posts = relationship("Post", back_populates="author")
# In Post model
author_id = Column(Integer, ForeignKey("users.id"))
author = relationship("User", back_populates="posts") -
One-to-Many: A Post has many Comments.
python# In Post model
comments = relationship("Comment", back_populates="post")
# In Comment model
post_id = Column(Integer, ForeignKey("posts.id"))
post = relationship("Post", back_populates="comments")
Advanced Model Features
Cascading Deletes
Notice the cascade="all, delete-orphan"
parameter in the Post-Comment relationship. This ensures that when a post is deleted, all its comments are deleted as well:
comments = relationship("Comment", back_populates="post", cascade="all, delete-orphan")
Automatic Timestamps
We've used SQLAlchemy's func.now()
to automatically set creation and update timestamps:
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
Indexes
Indexes speed up database queries. We've added them to commonly queried fields:
email = Column(String, unique=True, index=True)
username = Column(String, unique=True, index=True)
Real-World Example: Complete Blog API
Let's expand our example to include more API endpoints for a complete blog application:
# Add a comment to a post
@app.post("/posts/{post_id}/comments/", response_model=schemas.Comment)
def create_comment(
post_id: int,
user_id: int,
comment: schemas.CommentCreate,
db: Session = Depends(get_db)
):
# Check if post exists
post = db.query(models.Post).filter(models.Post.id == post_id).first()
if post is None:
raise HTTPException(status_code=404, detail="Post not found")
# Check if user exists
user = db.query(models.User).filter(models.User.id == user_id).first()
if user is None:
raise HTTPException(status_code=404, detail="User not found")
# Create comment
db_comment = models.Comment(
**comment.dict(), post_id=post_id, user_id=user_id
)
db.add(db_comment)
db.commit()
db.refresh(db_comment)
return db_comment
# Get a specific post with its comments
@app.get("/posts/{post_id}", response_model=schemas.PostWithUser)
def read_post(post_id: int, db: Session = Depends(get_db)):
post = db.query(models.Post).filter(models.Post.id == post_id).first()
if post is None:
raise HTTPException(status_code=404, detail="Post not found")
return post
# Update a post
@app.put("/posts/{post_id}", response_model=schemas.Post)
def update_post(
post_id: int,
post_update: schemas.PostCreate,
db: Session = Depends(get_db)
):
db_post = db.query(models.Post).filter(models.Post.id == post_id).first()
if db_post is None:
raise HTTPException(status_code=404, detail="Post not found")
# Update post attributes
for key, value in post_update.dict().items():
setattr(db_post, key, value)
db.commit()
db.refresh(db_post)
return db_post
# Delete a post
@app.delete("/posts/{post_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_post(post_id: int, db: Session = Depends(get_db)):
db_post = db.query(models.Post).filter(models.Post.id == post_id).first()
if db_post is None:
raise HTTPException(status_code=404, detail="Post not found")
db.delete(db_post)
db.commit()
return None
Best Practices for Database Models
-
Separate Concerns: Keep your database models, Pydantic schemas, and business logic separate.
-
Use Meaningful Names: Use descriptive names for models, columns, and relationships.
-
Add Proper Constraints: Use appropriate constraints like
unique=True
,nullable=False
, etc. -
Include Indexes: Add indexes to columns that are frequently queried.
-
Use Relationships Properly: Define relationships with appropriate cascade options.
-
Include Audit Trails: Add created and updated timestamps to track changes.
-
Handle Exceptions: Always handle database exceptions gracefully.
Summary
Database models are a crucial part of any FastAPI application that uses a database. In this guide, we've learned:
- How to define SQLAlchemy models for our database tables
- How to establish relationships between models
- How to use these models in FastAPI API endpoints
- Best practices for designing database models
With these skills, you can now build robust FastAPI applications that interact efficiently with databases.
Further Resources and Exercises
Resources
Exercises
-
Extend the Blog API: Add categories and tags to posts.
-
Implement Authentication: Add JWT authentication to the API.
-
Add Pagination: Implement proper pagination for listing endpoints.
-
Create Search Functionality: Implement a search endpoint for posts.
-
Add Soft Delete: Implement soft delete functionality instead of permanently removing records.
By completing these exercises, you'll gain a deeper understanding of working with database models in FastAPI applications.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)