像搭积木一样,一步步构建完整的 Web 服务
📦 本文使用的代码文件
在开始之前,请先创建以下三个核心文件,所有代码都将基于它们展开:
myproject/
├── main.py # FastAPI 主入口(包含所有路由)
├── database.py # 数据库配置(引擎、会话、依赖注入)
├── models.py # ORM 模型(Category 和 Post)
└── seed.py # 测试数据填充(可选)
前言
作为一名 Python 开发者,想要快速搭建一个 Web 应用,FastAPI + SQLAlchemy 的组合是目前最流行的选择之一。本文将用积木式的方式,带你从零开始,一步步搭建一个完整的博客文章管理系统。
你将学到:
- ✅ FastAPI 路由、请求处理、响应
- ✅ SQLAlchemy ORM 模型定义与数据库操作
- ✅ Pydantic 数据校验
- ✅ 依赖注入、中间件、启动事件
- ✅ 完整的项目结构
前置知识: Python 基础语法、了解 HTTP 协议基本概念
第一块积木:数据库连接(database.py)
📁 对应文件:
database.py(完整代码见文末)
1.1 创建数据库引擎
python
from sqlalchemy import create_engine
engine = create_engine(
'mysql+pymysql://root:123456@localhost:3306/pythondb',
echo=True, # 打印执行的 SQL(开发调试用)
pool_size=5, # 连接池大小
max_overflow=10 # 超出连接池后的最大连接数
)
# 测试连接
with engine.connect() as conn:
print("连接成功!")
🔑 关键点:
- 连接字符串格式:
mysql+pymysql://用户名:密码@主机:端口/数据库名 - 记得先在 MySQL 中创建对应的数据库
pythondb
1.2 创建会话工厂和基类
python
from sqlalchemy.orm import sessionmaker, declarative_base
# 会话工厂:用于创建数据库会话
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# ORM 基类:所有模型都继承它
Base = declarative_base()
💡 提示: 文件中还定义了一个
Base1类,那是另一种写法示例,实际使用时直接用Base即可。
1.3 依赖注入获取数据库会话
python
from fastapi import Depends
def get_db():
"""每个请求创建独立的数据库会话,请求结束后自动关闭"""
db = SessionLocal()
try:
yield db # 请求期间使用这个会话
finally:
db.close() # 防止连接泄漏
💡 为什么这样设计?
- 每个请求使用独立的会话,互不干扰
- 自动管理连接生命周期,避免资源泄漏
- 配合 FastAPI 的依赖注入系统使用
第二块积木:数据模型(models.py)
📁 对应文件:
models.py(完整代码见文末)
定义数据库表结构,就像画施工图纸一样。
2.1 分类表(Category)
python
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey
from sqlalchemy.orm import relationship
from datetime import datetime
from database import Base
class Category(Base):
"""文章分类"""
__tablename__ = "categories"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(50), unique=True, nullable=False)
slug = Column(String(50), unique=True, nullable=False)
# 与 Post 表建立双向关系
posts = relationship("Post", back_populates="category")
2.2 文章表(Post)
python
class Post(Base):
"""博客文章"""
__tablename__ = "posts"
id = Column(Integer, primary_key=True, index=True)
title = Column(String(200), nullable=False)
slug = Column(String(200), unique=True, nullable=False)
summary = Column(Text, default="")
content = Column(Text, nullable=False)
category_id = Column(Integer, ForeignKey("categories.id"), nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# 与 Category 的双向关系
category = relationship("Category", back_populates="posts")
📝 字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
id |
Integer | 主键,自动索引 |
name |
String(50) | 分类名称,唯一 |
slug |
String(200) | URL 友好的标识符,必须唯一 |
summary |
Text | 文章摘要,默认为空字符串 |
category_id |
Integer | 外键,关联 categories 表 |
created_at |
DateTime | 创建时间,自动填充当前时间 |
updated_at |
DateTime | 更新时间,每次修改自动更新 |
第三块积木:Pydantic 数据校验
📁 对应位置:
main.py
Pydantic 是 FastAPI 的数据校验核心,自动生成 API 文档 + 自动校验输入数据。
3.1 基础模型
python
from pydantic import BaseModel, EmailStr
class UserBase(BaseModel):
username: str # 必填
email: EmailStr # 必填,自动校验邮箱格式
full_name: str | None = None # 可选
3.2 输入输出分离
python
# 创建用户时的输入模型(含密码)
class UserCreate(UserBase):
password: str
# 返回用户信息的输出模型(不含密码)
class UserOut(UserBase):
id: int
@app.post("/users/", response_model=UserOut)
async def create_user(user: UserCreate):
# 响应时自动过滤密码字段
return {"id": 1, **user.model_dump(exclude={"password"})}
✨ 最佳实践: 输入和输出模型分开定义,避免敏感信息泄露
3.3 商品模型示例
python
class Item(BaseModel):
name: str
age: int = None # 可选字段
@app.post("/items1")
def items1(item: Item):
print(item.model_dump()) # 转为字典
print(item.model_dump_json()) # 转为 JSON 字符串
return item
第四块积木:FastAPI 路由(main.py)
📁 对应位置:
main.py
4.1 基础路由
python
from fastapi import FastAPI
app = FastAPI()
# GET 请求,路径参数
@app.get("/")
async def root():
return {"message": "Hello World"}
@app.get("/items/{item_id}")
def read_item(item_id: int, q1: str = None, p2: str = "p22"):
return {"item_id": item_id, "q": q1, "p2": p2}
# POST 请求,接收 JSON Body
@app.post("/items1")
def items1(item: Item):
return item
4.2 请求头和 Cookie
python
from fastapi import Header, Cookie
@app.get("/item2")
def item2(
user_agent: str = Header(None),
session_token: str = Cookie(None)
):
return {"User-Agent": user_agent, "Session-Token": session_token}
4.3 重定向和状态码
python
from fastapi.responses import RedirectResponse, JSONResponse
from fastapi import HTTPException
# 重定向
@app.get("/redirect")
def redirect():
return RedirectResponse(url="/item2/")
# 抛出 HTTP 异常
@app.get("/items1/{item_id}")
def read_item(item_id: int):
if item_id == 42:
raise HTTPException(status_code=404, detail="Item not found")
return {"item_id": item_id}
# 自定义响应头
@app.get("/default_header")
def default_header():
return JSONResponse(
content={"item_id": 1000, "body": 10001},
headers={"X-Custom-Header": "custom-value"}
)
4.4 表单数据处理
python
from fastapi import Form
@app.post("/login/")
async def login(
username: str = Form(), # 必填
password: str = Form(), # 必填
):
print(f"{password=}") # 调试输出
return {"username": username}
@app.post("/items/")
async def create_item(
name: str = Form(...), # ... 表示必填
description: str | None = Form(None),
price: float = Form(..., gt=0), # 必须大于 0
):
return {"name": name, "description": description, "price": price}
第五块积木:路由分组(APIRouter)
📁 对应位置:
main.py
当路由越来越多时,用 APIRouter 进行模块化管理。
python
from fastapi import APIRouter
router = APIRouter(
prefix="/posts", # 所有路由自动加上 /posts 前缀
tags=["文章"] # Swagger 文档分组
)
@router.get("/{post_id}", name="post_detail")
def post_detail(post_id: int):
"""文章详情"""
return {"post_id": post_id}
# 挂载到主应用
app.include_router(router)
第六块积木:启动事件 - 自动建表
📁 对应位置:
main.py
应用启动时自动创建数据库表,开发阶段非常方便。
python
from database import engine, Base
from models import Category, Post # 确保模型被导入
@app.on_event("startup")
def on_startup():
"""应用启动时自动创建数据库表(仅开发使用)"""
Base.metadata.create_all(bind=engine)
print("✅ 数据库表创建成功!")
⚠️ 注意: 如果表已存在,
create_all不会重复创建,只会创建不存在的表。
第七块积木:数据填充(seed.py)
⚠️ 注意:
seed.py文件在提供的代码中未给出完整实现 ,但在main.py中被引用。
python
# main.py 中的使用
import seed
@app.post("/insert_data")
async def insert_data():
seed.exec() # 执行数据填充
return "success..."
你需要自己创建 seed.py 文件,示例内容:
python
# seed.py - 你需要自行创建
from database import SessionLocal
from models import Category, Post
def exec():
db = SessionLocal()
try:
# 创建分类
cat = Category(name="Python", slug="python")
db.add(cat)
db.commit()
# 创建文章
post = Post(
title="FastAPI 入门",
slug="fastapi-intro",
content="这是内容...",
category_id=cat.id
)
db.add(post)
db.commit()
print("✅ 测试数据填充成功!")
finally:
db.close()
第八块积木:完整代码汇总
📄 database.py(完整)
python
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base
from fastapi import Depends
# MySQL连接字符串
engine = create_engine(
'mysql+pymysql://root:123456@localhost:3306/pythondb',
echo=True,
pool_size=5,
max_overflow=10
)
# 测试连接
with engine.connect() as conn:
print("连接成功!")
# 会话工厂
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# ORM 基类
Base = declarative_base()
# 依赖注入
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
📄 models.py(完整)
python
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey
from sqlalchemy.orm import relationship
from datetime import datetime
from database import Base
class Category(Base):
__tablename__ = "categories"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(50), unique=True, nullable=False)
slug = Column(String(50), unique=True, nullable=False)
posts = relationship("Post", back_populates="category")
class Post(Base):
__tablename__ = "posts"
id = Column(Integer, primary_key=True, index=True)
title = Column(String(200), nullable=False)
slug = Column(String(200), unique=True, nullable=False)
summary = Column(Text, default="")
content = Column(Text, nullable=False)
category_id = Column(Integer, ForeignKey("categories.id"), nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
category = relationship("Category", back_populates="posts")
📄 main.py(核心部分)
python
from fastapi import FastAPI, Depends, Header, Cookie, Form, HTTPException
from fastapi.responses import RedirectResponse, JSONResponse
from fastapi import APIRouter
from pydantic import BaseModel, EmailStr
import database, models, seed
app = FastAPI()
# ... 所有路由定义(参考第四、五部分)...
@app.on_event("startup")
def on_startup():
models.Base.metadata.create_all(bind=database.engine)
# 挂载路由
app.include_router(router)
快速启动
bash
# 1. 安装依赖
pip install fastapi uvicorn sqlalchemy pymysql pydantic
# 2. 创建数据库(MySQL)
mysql -u root -p
CREATE DATABASE pythondb;
# 3. 启动服务(热重载模式)
uvicorn main:app --reload
# 4. 访问交互式 API 文档
# http://127.0.0.1:8000/docs
总结:积木清单
| 积木 | 文件 | 作用 | 状态 |
|---|---|---|---|
| 🧱 数据库引擎 | database.py | 连接数据库 | ✅ 已提供 |
| 🧱 会话工厂 | database.py | 管理数据库会话 | ✅ 已提供 |
| 🧱 依赖注入 | database.py | 获取数据库会话 | ✅ 已提供 |
| 🧱 ORM 模型 | models.py | 定义表结构 | ✅ 已提供 |
| 🧱 Pydantic 模型 | main.py | 数据校验 | ✅ 已提供 |
| 🧱 路由定义 | main.py | API 端点 | ✅ 已提供 |
| 🧱 路由分组 | main.py | 模块化管理 | ✅ 已提供 |
| 🧱 启动事件 | main.py | 自动建表 | ✅ 已提供 |
| 🧱 数据填充 | seed.py | 测试数据 | ⚠️ 需自行创建 |
⚠️ 注意事项
-
seed.py 文件未提供 :
main.py中引用了import seed和seed.exec(),你需要自行创建该文件,或注释掉相关路由。 -
修改数据库配置 :将
database.py中的连接字符串改为你自己的 MySQL 配置。 -
生产环境注意:
- 关闭
echo=True - 不要在生产环境使用
create_all() - 使用环境变量管理敏感信息
- 关闭
下一步可以学什么?
- 关联查询 :学习
joinedload预加载关联数据 - 分页查询 :实现
offset/limit分页 - 中间件:添加日志、CORS、认证
- 异步支持 :使用
async/await处理高并发 - 单元测试 :用
TestClient测试 API
💡 记住: 编程就像搭积木,先把基础模块理解透彻,再逐步组合成复杂的系统。本文的每个代码块都是独立的积木,你可以随时取用!
如果你在实践过程中遇到问题,欢迎在评论区留言交流! 🚀