Skip to content

Commit 7675616

Browse files
stackjayjaygaha
authored andcommitted
day #48 fastapi #9 database integration with SQLAlchemy
1 parent ab4b31c commit 7675616

15 files changed

Lines changed: 459 additions & 0 deletions

File tree

workspace/7_framework/fastapi/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ uvicorn main:app --reload
9797
Learn how to handle advanced file uploads, serve static files, and build a complete file management system with features like validation, image processing, and searching.
9898
_Includes: single and multiple file uploads, static file serving, validation, image thumbnail generation, and a complete file management API._
9999

100+
- [Day 09: Database Integration with SQLAlchemy](day09/README.md)
101+
Learn how to integrate a SQL database with FastAPI using SQLAlchemy, manage database sessions, define models and schemas, and implement CRUD operations.
102+
_Includes: SQLAlchemy setup, session management, CRUD utils, and testing with a SQLite database._
103+
100104
---
101105

102106
## Recommended Project Structure
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DATABASE_URL=sqlite:///./test.db
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# FastAPI Day 09: Database Integration with SQLAlchemy
2+
3+
Welcome to **Day 09** of the FastAPI tutorial series! Building on the concepts from previous days, today we'll integrate a database into our application. You'll learn how to set up a connection, define data models, and perform CRUD (Create, Read, Update, Delete) operations using SQLAlchemy, the de facto standard for database interaction in the Python ecosystem.
4+
5+
---
6+
7+
## What You'll Learn
8+
9+
- Connect a FastAPI application to a SQL database using SQLAlchemy.
10+
- Define database tables using SQLAlchemy's ORM (Object-Relational Mapper).
11+
- Manage database configuration securely using `.env` files and Pydantic settings.
12+
- Structure a database-driven application by separating concerns into modules.
13+
- Understand the critical difference between SQLAlchemy models and Pydantic schemas.
14+
- Implement a dependency injection system for managing database sessions.
15+
- Write unit tests for API endpoints that interact with a database.
16+
17+
---
18+
19+
## Key Concepts
20+
21+
### 1. Application Structure
22+
23+
Just as we separated file handling logic in Day 08, this project uses a modular structure to keep the codebase clean and maintainable.
24+
25+
- `main.py`: The application's entry point. It initializes the FastAPI app, creates the database tables, and includes the feature-specific routers.
26+
- `database/`: A dedicated module for all database-related configuration and session management.
27+
- `config.py`: Uses `pydantic-settings` to load the `DATABASE_URL` from the `.env` file. This avoids hardcoding sensitive credentials.
28+
- `database.py`: Contains the core SQLAlchemy setup: the `engine`, the `SessionLocal` factory for creating sessions, and the declarative `Base` class that our ORM models will inherit from.
29+
- `newsletter/`: A module representing a single feature of our application (a newsletter subscription service).
30+
- `models.py`: Defines the SQLAlchemy ORM models (e.g., `NewsletterSubscription`), which map to database tables.
31+
- `schemas.py`: Defines the Pydantic models used for data validation and serialization in API requests and responses.
32+
- `routes.py`: Contains the API endpoints (`/subscribe`, `/unsubscribe`, etc.).
33+
- `utils.py`: Holds the business logic (the CRUD functions) that interacts with the database.
34+
- `tests/`: Contains unit tests for the API, now adapted to work with a test database.
35+
36+
### 2. Database Configuration (`database/config.py`)
37+
38+
We use `pydantic-settings` to manage configuration. This allows us to define our required environment variables in a Pydantic model. It automatically reads from an `.env` file, providing a robust way to configure the application without exposing secrets in the code.
39+
40+
```python-beginner/workspace/7_framework/fastapi/day09/database/config.py#L1-L8
41+
from pydantic_settings import BaseSettings
42+
43+
class Settings(BaseSettings):
44+
DATABASE_URL: str
45+
class Config:
46+
env_file = ".env"
47+
48+
settings = Settings()
49+
```
50+
51+
### 3. Session Management and Dependency Injection (`database/database.py`)
52+
53+
Efficiently managing database connections is critical. This project uses FastAPI's dependency injection system to handle sessions:
54+
55+
- **`engine`**: The central point of communication with the database.
56+
- **`SessionLocal`**: A factory that creates new database session objects.
57+
- **`get_db`**: A dependency function (and a generator) that creates a new session for each incoming request, passes it to the path operation function, and guarantees that the session is closed afterward, even if an error occurs.
58+
59+
```python-beginner/workspace/7_framework/fastapi/day09/database/database.py#L22-L28
60+
def get_db():
61+
db = SessionLocal()
62+
try:
63+
yield db
64+
finally:
65+
db.close()
66+
```
67+
This dependency is then injected into our route handlers, ensuring every request gets its own isolated session.
68+
69+
### 4. Models vs. Schemas: A Critical Distinction
70+
71+
This is one of the most important concepts when combining FastAPI and SQLAlchemy.
72+
73+
- **SQLAlchemy Models (`newsletter/models.py`)**: These are Python classes that map to database tables. They define the table name and columns. They are the source of truth for your database structure and are used directly by your business logic (`utils.py`) to query and manipulate data.
74+
75+
```python-beginner/workspace/7_framework/fastapi/day09/newsletter/models.py#L5-L11
76+
class NewsletterSubscription(Base):
77+
__tablename__ = "newsletter_subscriptions"
78+
id = Column(Integer, primary_key=True, index=True)
79+
email = Column(String(255) , unique=True , nullable=True)
80+
is_active = Column(Boolean, default=True)
81+
created_at = Column(DateTime, default=datetime.now)
82+
```
83+
84+
- **Pydantic Schemas (`newsletter/schemas.py`)**: These are Pydantic models that define the shape of the data for your API. They are used for request body validation and for formatting response data. By using schemas, you create a clear and secure API contract, preventing accidental exposure of database model fields that should not be sent to the client.
85+
86+
```python-beginner/workspace/7_framework/fastapi/day09/newsletter/schemas.py#L11-L18
87+
class NewsletterSubscriptionResponse(NewsletterSubscriptionBase):
88+
id: int
89+
is_active: bool
90+
created_at: datetime
91+
92+
class Config:
93+
from_attributes = True
94+
```
95+
The `Config.from_attributes = True` (formerly `orm_mode`) tells Pydantic to read the data from ORM model attributes, not just dictionaries.
96+
97+
### 5. Testing with a Database
98+
99+
Testing code that interacts with a database requires a special setup to ensure tests are isolated and don't interfere with each other or with the development database.
100+
101+
Our `tests/test_newsletter.py` demonstrates how to:
102+
- **Use a Test Database**: It creates an in-memory SQLite database for the duration of the test run.
103+
- **Override Dependencies**: It uses `app.dependency_overrides` to replace the main `get_db` dependency with one that connects to the test database.
104+
- **Use Fixtures for Setup/Teardown**: A `pytest` fixture is used to create the database schema before each test and tear it down afterward, ensuring every test starts with a clean slate.
105+
106+
---
107+
108+
## Next Steps
109+
110+
- Examine the `.env` file and see how the `DATABASE_URL` is defined. Try changing it to a different SQLite file path.
111+
- Run the application with `uvicorn main:app --reload` and use an API client to test the `/newsletter/subscribe` and `/newsletter/subscriptions` endpoints.
112+
- Run the tests with `python -m pytest` to see the automated testing in action.
113+
- Trace the lifecycle of a request: follow a call from a function in `routes.py`, to the business logic in `utils.py`, and see how it uses the SQLAlchemy model from `models.py` and the Pydantic schema from `schemas.py`.
114+
115+
---

workspace/7_framework/fastapi/day09/database/__init__.py

Whitespace-only changes.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from pydantic_settings import BaseSettings
2+
3+
class Settings(BaseSettings):
4+
DATABASE_URL: str
5+
class Config:
6+
env_file = ".env"
7+
8+
settings = Settings()
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from sqlalchemy import create_engine
2+
from sqlalchemy.ext.declarative import declarative_base
3+
from sqlalchemy.orm import sessionmaker
4+
from .config import settings
5+
6+
SQLALCHEMY_DATABASE_URL = settings.DATABASE_URL
7+
8+
# Establishing database connection
9+
engine = create_engine(
10+
SQLALCHEMY_DATABASE_URL,
11+
connect_args={"check_same_thread": False}
12+
)
13+
14+
# Managing database sessions
15+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
16+
17+
# Create a base class for declarative models
18+
Base = declarative_base()
19+
20+
def get_db():
21+
db = SessionLocal()
22+
try:
23+
yield db
24+
finally:
25+
db.close()
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from fastapi import FastAPI
2+
from fastapi.middleware.cors import CORSMiddleware
3+
from database.database import get_db , engine , Base
4+
import os
5+
import uvicorn
6+
from newsletter.routes import router as newsletter_route
7+
8+
9+
app = FastAPI()
10+
Base.metadata.create_all(bind=engine)
11+
app.include_router(newsletter_route)
12+
13+
app.add_middleware(
14+
CORSMiddleware,
15+
allow_origins=["*"],
16+
allow_credentials=True,
17+
allow_methods=["*"],
18+
allow_headers=["*"],
19+
)
20+
21+
22+
if __name__ == "__main__":
23+
uvicorn.run(
24+
"main:app",
25+
host="0.0.0.0",
26+
port=8880,
27+
reload=True,
28+
reload_dirs=[os.path.dirname(os.path.abspath(__file__))],
29+
reload_excludes=[
30+
"*/.git/*",
31+
"*/__pycache__/*",
32+
"*.pyc",
33+
"*/.pytest_cache/*",
34+
"*/.vscode/*",
35+
"*/.idea/*"
36+
],
37+
reload_delay=1,
38+
reload_includes=["*.py", "*.html", "*.css", "*.js"]
39+
)

workspace/7_framework/fastapi/day09/newsletter/__init__.py

Whitespace-only changes.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from sqlalchemy import Column, Integer, String, Boolean, DateTime
2+
from database.database import Base
3+
from datetime import datetime
4+
5+
class NewsletterSubscription(Base):
6+
__tablename__ = "newsletter_subscriptions"
7+
id = Column(Integer, primary_key=True, index=True)
8+
email = Column(String(255) , unique=True , nullable=True)
9+
is_active = Column(Boolean, default=True)
10+
created_at = Column(DateTime, default=datetime.now)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from fastapi import APIRouter, Depends, HTTPException, status
2+
from sqlalchemy.orm import Session
3+
from database.database import get_db
4+
from .schemas import NewsletterSubscriptionBase , NewsletterSubscriptionCreate , NewsletterSubscriptionResponse
5+
from .utils import create_subscription , get_all_subscriptions , delete_subscription
6+
router = APIRouter(prefix="/newsletter", tags=["newsletter"])
7+
8+
@router.post("/subscribe", response_model=NewsletterSubscriptionResponse)
9+
def subscribe(
10+
subscription: NewsletterSubscriptionCreate,
11+
db: Session = Depends(get_db)
12+
):
13+
14+
return create_subscription(db, subscription)
15+
16+
@router.get("/subscriptions", response_model=list[NewsletterSubscriptionResponse])
17+
def get_subscriptions(
18+
skip: int = 0,
19+
limit: int = 100,
20+
db: Session = Depends(get_db)
21+
):
22+
return get_all_subscriptions(db, skip, limit)
23+
24+
@router.delete("/unsubscribe/{email}")
25+
def unsubscribe(email: str, db: Session = Depends(get_db)):
26+
if delete_subscription(db, email):
27+
return {"message": "Successfully unsubscribed"}
28+
raise HTTPException(
29+
status_code=status.HTTP_404_NOT_FOUND,
30+
detail="Subscription not found"
31+
)

0 commit comments

Comments
 (0)