13.后端开发基础

1. 数据库设计

1.1 为什么需要选择合适的数据类型?

问题:设计数据库表时,如何选择字段的数据类型?

生活类比

  • 用大箱子装小东西:浪费空间,搬运困难
  • 用小箱子装大东西:装不下,需要换箱子
  • 用合适的箱子:既不浪费,又能装下

实际例子

1
2
3
4
5
6
7
-- ❌ 不好的选择
age TINYINT -- 年龄用TINYINT(-128到127),足够了
status INT -- 状态用INT(-21亿到21亿),太浪费了!

-- ✅ 好的选择
age TINYINT UNSIGNED -- 年龄0-255,足够用
status TINYINT -- 状态0-255,足够用(0=未激活,1=已激活,2=已删除)

选择合适数据类型的好处

  • 节省存储空间:数据量大的时候,节省的空间很可观
  • 提高查询速度:数据类型越小,查询越快
  • 避免数据错误:限制数据范围,防止无效数据

1.2 MySQL整数类型

1.2.1 整数类型对比

类型 字节 有符号范围 无符号范围 适用场景
TINYINT 1 -128 ~ 127 0 ~ 255 年龄、状态码、是否删除
SMALLINT 2 -32768 ~ 32767 0 ~ 65535 年份、小数量
MEDIUMINT 3 -8388608 ~ 8388607 0 ~ 16777215 中等数量
INT 4 -21亿 ~ 21亿 0 ~ 42亿 用户ID、文章ID(常用)
BIGINT 8 超大范围 超大范围 超大数据量、时间戳

1.2.2 实际选型例子

用户ID(主键)

1
2
3
4
-- ✅ 推荐:INT UNSIGNED
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY
-- 范围:0 ~ 42亿,足够大多数应用使用
-- 如果用户量可能超过42亿,用BIGINT

年龄

1
2
3
4
-- ✅ 推荐:TINYINT UNSIGNED
age TINYINT UNSIGNED
-- 范围:0 ~ 255,足够存储年龄
-- 节省空间:1字节 vs INT的4字节,节省75%空间

状态码

1
2
3
4
5
-- ✅ 推荐:TINYINT
status TINYINT DEFAULT 0
-- 0=未激活,1=已激活,2=已删除,3=已禁用
-- 范围:-128 ~ 127,足够用
-- 如果只用0和1,可以用TINYINT(1)或BOOLEAN

年份

1
2
3
4
-- ✅ 推荐:SMALLINT UNSIGNED
year SMALLINT UNSIGNED
-- 范围:0 ~ 65535,足够存储年份(1900-65535)
-- 比INT节省一半空间

数量(如库存、点赞数)

1
2
3
4
5
6
7
8
-- 小数量(< 65535)
stock SMALLINT UNSIGNED DEFAULT 0

-- 中等数量(< 1677万)
view_count MEDIUMINT UNSIGNED DEFAULT 0

-- 大数量(可能超过1677万)
like_count INT UNSIGNED DEFAULT 0

1.3 MySQL字符串类型

1.3.1 定长字符串:CHAR

CHAR(n) = 固定长度的字符串,n为字符数(1-255)

特点

  • ✅ 固定长度,存储效率高
  • ✅ 适合存储长度固定的数据
  • ❌ 浪费空间(如果实际数据小于n)

实际例子

1
2
3
4
-- ✅ 适合用CHAR的场景
country_code CHAR(2) -- 国家代码:CN, US, JP
phone_code CHAR(3) -- 电话区号:010, 021
status_code CHAR(1) -- 状态码:A, B, C

1.3.2 变长字符串:VARCHAR

VARCHAR(n) = 可变长度的字符串,n为最大字符数(1-65535)

特点

  • ✅ 按实际长度存储,节省空间
  • ✅ 适合存储长度不固定的数据
  • ❌ 需要额外1-2字节存储长度信息

实际例子

1
2
3
4
5
-- ✅ 适合用VARCHAR的场景
name VARCHAR(50) -- 姓名:通常10-20个字符
email VARCHAR(100) -- 邮箱:通常20-50个字符
title VARCHAR(200) -- 标题:通常10-100个字符
address VARCHAR(255) -- 地址:通常50-200个字符

VARCHAR长度选择原则

数据长度 推荐VARCHAR长度 说明
10-20字符 VARCHAR(50) 预留2-3倍空间,方便扩展
20-50字符 VARCHAR(100) 预留2倍空间
50-100字符 VARCHAR(200) 预留2倍空间
100-200字符 VARCHAR(255) 最大常用长度

1.4 MySQL文本类型

1.4.1 文本类型对比

类型 最大长度 存储空间 适用场景
TINYTEXT 255字符 1字节+实际长度 短文本、备注
TEXT 65,535字符(64KB) 2字节+实际长度 文章内容、评论内容(常用)
MEDIUMTEXT 16,777,215字符(16MB) 3字节+实际长度 长文章、富文本内容
LONGTEXT 4,294,967,295字符(4GB) 4字节+实际长度 超长文本、文档内容

1.4.2 实际选型例子

文章摘要/简介

1
2
3
4
-- ✅ 推荐:TINYTEXT或VARCHAR(255)
summary TINYTEXT
-- 摘要通常100-200字符,TINYTEXT足够
-- 或者用VARCHAR(255)也可以

文章内容

1
2
3
4
-- ✅ 推荐:TEXT
content TEXT
-- 普通文章通常几千到几万字,TEXT(64KB)足够
-- 如果文章可能很长(如技术文档),用MEDIUMTEXT

富文本内容

1
2
3
4
-- ✅ 推荐:MEDIUMTEXT
rich_content MEDIUMTEXT
-- 富文本包含HTML标签,可能很长
-- 16MB足够存储大多数富文本内容

评论内容

1
2
3
4
-- ✅ 推荐:TEXT
comment TEXT
-- 评论通常几百到几千字,TEXT足够
-- 如果评论可能很长,用MEDIUMTEXT

注意事项

  • 不要用VARCHAR存储长文本:VARCHAR最大65535字节,但实际受行大小限制
  • 不确定长度时,选大不选小:TEXT不够用再升级到MEDIUMTEXT
  • TEXT类型不能设置默认值:MySQL不支持

1.5 MySQL日期时间类型

1.5.1 日期时间类型对比

类型 格式 范围 存储空间 适用场景
DATE YYYY-MM-DD 1000-01-01 ~ 9999-12-31 3字节 生日、日期
TIME HH:MM:SS -838:59:59 ~ 838:59:59 3字节 时间点
DATETIME YYYY-MM-DD HH:MM:SS 1000-01-01 00:00:00 ~ 9999-12-31 23:59:59 8字节 创建时间、更新时间(常用)
TIMESTAMP YYYY-MM-DD HH:MM:SS 1970-01-01 00:00:00 ~ 2038-01-19 03:14:07 4字节 自动更新时间戳
YEAR YYYY 1901 ~ 2155 1字节 年份(节省空间)

1.5.2 实际选型例子

创建时间、更新时间

1
2
3
4
5
6
7
-- ✅ 推荐:DATETIME
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP

-- 或者用TIMESTAMP(更省空间,但范围有限)
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP

生日

1
2
3
4
-- ✅ 推荐:DATE
birthday DATE
-- 只需要日期,不需要时间
-- 比DATETIME节省空间

年份

1
2
3
4
-- ✅ 推荐:YEAR(如果只需要年份)
publish_year YEAR
-- 只需要年份时,用YEAR最省空间(1字节)
-- 或者用SMALLINT UNSIGNED也可以

DATETIME vs TIMESTAMP

特性 DATETIME TIMESTAMP
范围 1000-9999年 1970-2038年
存储空间 8字节 4字节
时区 不涉及时区 自动转换时区
推荐 ✅ 大多数场景 ⚠️ 需要时区转换时

1.6 MySQL其他常用类型

1.6.1 布尔类型

BOOLEAN / BOOL = TINYINT(1)的别名

实际例子

1
2
3
4
-- ✅ 推荐:TINYINT(1)或BOOLEAN
is_deleted TINYINT(1) DEFAULT 0 -- 0=未删除,1=已删除
is_active BOOLEAN DEFAULT TRUE -- TRUE=1,FALSE=0
completed TINYINT(1) DEFAULT 0 -- 0=未完成,1=已完成

1.6.2 浮点数类型

类型 精度 存储空间 适用场景
FLOAT 单精度 4字节 一般精度要求不高的浮点数
DOUBLE 双精度 8字节 高精度浮点数(常用)
DECIMAL(M,D) 精确小数 变长 金额、价格(必须精确)

实际例子

1
2
3
4
5
6
7
8
9
10
-- ✅ 价格、金额:必须用DECIMAL
price DECIMAL(10, 2) -- 总长度10位,小数2位,如:99999999.99
amount DECIMAL(15, 2) -- 总长度15位,小数2位

-- ⚠️ 一般浮点数:用DOUBLE
score DOUBLE -- 分数:可能有小数
rate DOUBLE -- 比率:可能有小数

-- ❌ 不要用FLOAT/DOUBLE存储金额
-- 因为浮点数有精度问题,可能导致计算错误

1.6.3 JSON类型

JSON = 存储JSON格式的数据(MySQL 5.7+)

实际例子

1
2
3
-- ✅ 适合存储灵活的配置、扩展字段
config JSON -- 配置信息:{"theme": "dark", "lang": "zh"}
metadata JSON -- 元数据:{"tags": ["tag1", "tag2"], "views": 100}

注意事项

  • ✅ 适合存储结构不固定的数据
  • ❌ 不适合频繁查询的字段(查询性能较差)
  • ✅ 可以用JSON函数查询:JSON_EXTRACT(config, '$.theme')

1.7 实际例子:博客系统表设计

用户表(users)

1
2
3
4
5
6
7
8
9
10
CREATE TABLE users (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, -- 用户ID,INT足够
username VARCHAR(50) NOT NULL UNIQUE, -- 用户名,50字符足够
email VARCHAR(100) NOT NULL UNIQUE, -- 邮箱,100字符足够
password VARCHAR(255) NOT NULL, -- 密码(加密后),255字符
age TINYINT UNSIGNED, -- 年龄,0-255足够
status TINYINT DEFAULT 1, -- 状态:0=禁用,1=正常,2=删除
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

文章表(articles)

1
2
3
4
5
6
7
8
9
10
11
12
13
CREATE TABLE articles (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, -- 文章ID
title VARCHAR(200) NOT NULL, -- 标题,200字符足够
summary TINYTEXT, -- 摘要,255字符足够
content TEXT NOT NULL, -- 内容,64KB足够大多数文章
author_id INT UNSIGNED NOT NULL, -- 作者ID,关联users表
view_count INT UNSIGNED DEFAULT 0, -- 浏览量,可能很大,用INT
like_count INT UNSIGNED DEFAULT 0, -- 点赞数,可能很大,用INT
status TINYINT DEFAULT 1, -- 状态:0=草稿,1=发布,2=删除
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (author_id) REFERENCES users(id)
);

评论表(comments)

1
2
3
4
5
6
7
8
9
10
11
CREATE TABLE comments (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
article_id INT UNSIGNED NOT NULL, -- 文章ID
user_id INT UNSIGNED NOT NULL, -- 用户ID
content TEXT NOT NULL, -- 评论内容,TEXT足够
like_count SMALLINT UNSIGNED DEFAULT 0, -- 点赞数,通常不会太大,用SMALLINT
is_deleted TINYINT(1) DEFAULT 0, -- 是否删除,布尔值
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (article_id) REFERENCES articles(id),
FOREIGN KEY (user_id) REFERENCES users(id)
);

1.8 数据类型选择总结

选择原则

  1. 够用即可:不要选择过大的类型,浪费空间
  2. 留有余地:不要选择过小的类型,避免后续扩展困难
  3. 考虑性能:数据类型越小,查询越快
  4. 考虑业务:根据实际业务需求选择

快速参考表

数据 推荐类型 说明
主键ID INT UNSIGNED 大多数场景足够
年龄 TINYINT UNSIGNED 0-255足够
状态码 TINYINT -128~127足够
年份 YEAR或SMALLINT UNSIGNED YEAR最省空间
姓名 VARCHAR(50) 预留2-3倍空间
邮箱 VARCHAR(100) 通常20-50字符
标题 VARCHAR(200) 通常10-100字符
短文本 TINYTEXT < 255字符
文章内容 TEXT 大多数文章足够
长文章 MEDIUMTEXT 超长文章
创建时间 DATETIME 常用
生日 DATE 只需要日期
价格 DECIMAL(10,2) 必须精确
是否删除 TINYINT(1) 布尔值

2. API实现

2.1 为什么需要后端API?

前端和后端的分工

角色 职责
前端 展示界面,处理用户交互
后端 处理业务逻辑,操作数据库

实际例子

1
2
3
4
5
6
7
用户在前端点击"发布文章"按钮:
1. 前端:收集表单数据(标题、内容)
2. 前端:发送请求到后端API(POST /api/articles)
3. 后端:验证数据(标题不能为空)
4. 后端:保存到数据库
5. 后端:返回结果给前端
6. 前端:显示成功提示

2.2 使用FastAPI实现CRUD接口

2.2.1 FastAPI简介

FastAPI = 一个现代的Python Web框架,用于快速构建API

特点

  • ✅ 简单易学:代码简洁,容易理解
  • ✅ 自动文档:自动生成API文档
  • ✅ 性能好:运行速度快

2.2.2 创建FastAPI项目

基本结构

1
2
3
4
5
6
7
8
9
10
11
12
from fastapi import FastAPI

# 创建FastAPI应用
app = FastAPI()

# 定义路由
@app.get("/")
def read_root():
return {"message": "Hello World"}

# 运行应用
# uvicorn main:app --reload

2.2.3 实现CRUD接口

实际例子:实现用户管理的CRUD接口

1. 创建用户(Create)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List

app = FastAPI()

# 定义数据模型
class UserCreate(BaseModel):
name: str
email: str
password: str

class UserResponse(BaseModel):
id: int
name: str
email: str

# 模拟数据库(实际项目中应该用真实数据库)
users_db = []
next_id = 1

# 创建用户
@app.post("/api/users", response_model=UserResponse)
def create_user(user: UserCreate):
global next_id

# 检查邮箱是否已存在
for u in users_db:
if u["email"] == user.email:
raise HTTPException(status_code=400, detail="邮箱已存在")

# 创建新用户
new_user = {
"id": next_id,
"name": user.name,
"email": user.email,
"password": user.password # 实际项目中应该加密
}
users_db.append(new_user)
next_id += 1

return new_user

2. 获取用户列表(Read - 列表)

1
2
3
4
# 获取用户列表
@app.get("/api/users", response_model=List[UserResponse])
def get_users():
return users_db

3. 获取单个用户(Read - 详情)

1
2
3
4
5
6
7
8
9
10
# 获取单个用户
@app.get("/api/users/{user_id}", response_model=UserResponse)
def get_user(user_id: int):
# 查找用户
for user in users_db:
if user["id"] == user_id:
return user

# 用户不存在
raise HTTPException(status_code=404, detail="用户不存在")

4. 更新用户(Update)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class UserUpdate(BaseModel):
name: str = None
email: str = None

# 更新用户
@app.put("/api/users/{user_id}", response_model=UserResponse)
def update_user(user_id: int, user_update: UserUpdate):
# 查找用户
for user in users_db:
if user["id"] == user_id:
# 更新字段
if user_update.name:
user["name"] = user_update.name
if user_update.email:
user["email"] = user_update.email
return user

# 用户不存在
raise HTTPException(status_code=404, detail="用户不存在")

5. 删除用户(Delete)

1
2
3
4
5
6
7
8
9
10
11
# 删除用户
@app.delete("/api/users/{user_id}")
def delete_user(user_id: int):
# 查找用户
for i, user in enumerate(users_db):
if user["id"] == user_id:
users_db.pop(i)
return {"message": "删除成功"}

# 用户不存在
raise HTTPException(status_code=404, detail="用户不存在")

2.3 接口测试

2.3.1 启动服务

1
2
3
4
5
# 安装FastAPI
pip install fastapi uvicorn

# 启动服务
uvicorn main:app --reload

2.3.2 使用APIFox测试

测试创建用户

1
2
3
4
5
6
7
8
方法:POST
URL:http://localhost:8000/api/users
请求体(JSON):
{
"name": "张三",
"email": "zhangsan@example.com",
"password": "123456"
}

测试获取用户列表

1
2
方法:GET
URL:http://localhost:8000/api/users

测试获取单个用户

1
2
方法:GET
URL:http://localhost:8000/api/users/1

测试更新用户

1
2
3
4
5
6
方法:PUT
URL:http://localhost:8000/api/users/1
请求体(JSON):
{
"name": "张三(已更新)"
}

测试删除用户

1
2
方法:DELETE
URL:http://localhost:8000/api/users/1

2.4 连接真实数据库

2.4.1 使用SQLAlchemy

SQLAlchemy = Python的数据库ORM工具,可以方便地操作数据库

安装

1
pip install sqlalchemy

基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

# 创建数据库连接
DATABASE_URL = "sqlite:///./test.db" # SQLite数据库
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

# 定义数据模型
class User(Base):
__tablename__ = "users"

id = Column(Integer, primary_key=True, index=True)
name = Column(String(50))
email = Column(String(100), unique=True, index=True)
password = Column(String(255))

# 创建表
Base.metadata.create_all(bind=engine)

# 使用数据库
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()

2.4.2 使用数据库的CRUD接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from fastapi import Depends
from sqlalchemy.orm import Session

# 创建用户(使用数据库)
@app.post("/api/users", response_model=UserResponse)
def create_user(user: UserCreate, db: Session = Depends(get_db)):
# 检查邮箱是否已存在
db_user = db.query(User).filter(User.email == user.email).first()
if db_user:
raise HTTPException(status_code=400, detail="邮箱已存在")

# 创建新用户
db_user = User(
name=user.name,
email=user.email,
password=user.password
)
db.add(db_user)
db.commit()
db.refresh(db_user)

return db_user

# 获取用户列表(使用数据库)
@app.get("/api/users", response_model=List[UserResponse])
def get_users(db: Session = Depends(get_db)):
users = db.query(User).all()
return users

2.5 接口设计要点

2.5.1 统一响应格式

标准响应格式

1
2
3
4
5
{
"code": 200, # 状态码
"message": "成功", # 提示信息
"data": { ... } # 数据
}

实际例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 成功响应
{
"code": 200,
"message": "创建成功",
"data": {
"id": 1,
"name": "张三",
"email": "zhangsan@example.com"
}
}

# 错误响应
{
"code": 400,
"message": "邮箱已存在",
"data": null
}

2.5.2 错误处理

常见错误

错误类型 HTTP状态码 说明
参数错误 400 请求参数不正确
未授权 401 未登录或Token无效
禁止访问 403 没有权限
资源不存在 404 请求的资源不存在
服务器错误 500 服务器内部错误

实际例子

1
2
3
4
5
6
7
# 用户不存在
if not user:
raise HTTPException(status_code=404, detail="用户不存在")

# 邮箱已存在
if db.query(User).filter(User.email == email).first():
raise HTTPException(status_code=400, detail="邮箱已存在")

2.6 实际例子:待办事项API

完整的待办事项CRUD接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List, Optional
from datetime import datetime

app = FastAPI()

# 数据模型
class TodoCreate(BaseModel):
title: str

class TodoUpdate(BaseModel):
title: Optional[str] = None
completed: Optional[bool] = None

class TodoResponse(BaseModel):
id: int
title: str
completed: bool
created_at: datetime

# 模拟数据库
todos_db = []
next_id = 1

# 创建待办事项
@app.post("/api/todos", response_model=TodoResponse)
def create_todo(todo: TodoCreate):
global next_id
new_todo = {
"id": next_id,
"title": todo.title,
"completed": False,
"created_at": datetime.now()
}
todos_db.append(new_todo)
next_id += 1
return new_todo

# 获取待办事项列表
@app.get("/api/todos", response_model=List[TodoResponse])
def get_todos():
return todos_db

# 获取单个待办事项
@app.get("/api/todos/{todo_id}", response_model=TodoResponse)
def get_todo(todo_id: int):
for todo in todos_db:
if todo["id"] == todo_id:
return todo
raise HTTPException(status_code=404, detail="待办事项不存在")

# 更新待办事项
@app.put("/api/todos/{todo_id}", response_model=TodoResponse)
def update_todo(todo_id: int, todo_update: TodoUpdate):
for todo in todos_db:
if todo["id"] == todo_id:
if todo_update.title:
todo["title"] = todo_update.title
if todo_update.completed is not None:
todo["completed"] = todo_update.completed
return todo
raise HTTPException(status_code=404, detail="待办事项不存在")

# 删除待办事项
@app.delete("/api/todos/{todo_id}")
def delete_todo(todo_id: int):
for i, todo in enumerate(todos_db):
if todo["id"] == todo_id:
todos_db.pop(i)
return {"message": "删除成功"}
raise HTTPException(status_code=404, detail="待办事项不存在")

💡 关键理解
后端API的核心就是实现CRUD操作,将前端请求转换为数据库操作,并返回结果。
掌握了CRUD接口的实现,就掌握了后端开发的基础!


3. 数据验证

3.1 为什么需要数据验证?

问题:前端传来的数据一定正确吗?

实际情况

  • ❌ 用户可能输入错误的数据
  • ❌ 前端可能被绕过(直接调用API)
  • ❌ 恶意用户可能发送恶意数据

生活类比

  • 没有验证:任何人都能进入,不安全
  • 有验证:检查身份和权限,安全可靠

实际例子

1
2
3
4
5
6
7
8
9
10
11
# ❌ 没有验证的接口
@app.post("/api/users")
def create_user(user: dict):
# 直接保存,可能有问题
save_user(user) # 如果user没有name字段怎么办?

# ✅ 有验证的接口
@app.post("/api/users")
def create_user(user: UserCreate):
# Pydantic自动验证,确保数据正确
save_user(user) # 如果数据不对,会自动报错

3.2 使用Pydantic进行数据验证

3.2.1 Pydantic简介

Pydantic = Python的数据验证库,FastAPI内置支持

特点

  • ✅ 自动验证数据类型
  • ✅ 自动验证数据格式
  • ✅ 自动生成错误信息
  • ✅ 代码简洁易读

3.2.2 基本验证

定义数据模型

1
2
3
4
5
6
7
8
from pydantic import BaseModel, EmailStr
from typing import Optional

class UserCreate(BaseModel):
name: str # 必填,字符串类型
email: EmailStr # 必填,必须是邮箱格式
age: Optional[int] = None # 可选,整数类型
password: str # 必填,字符串类型

自动验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# ✅ 正确的数据
user = UserCreate(
name="张三",
email="zhangsan@example.com",
age=20,
password="123456"
)

# ❌ 错误的数据(会自动报错)
user = UserCreate(
name="张三",
email="not-an-email", # 不是邮箱格式,会报错
age="20" # 不是整数,会报错
)

3.3 常用验证规则

3.3.1 字符串验证

长度限制

1
2
3
4
5
6
7
8
9
10
from pydantic import BaseModel, Field

class UserCreate(BaseModel):
name: str = Field(..., min_length=2, max_length=50)
# ... 表示必填
# min_length=2:最少2个字符
# max_length=50:最多50个字符

email: str = Field(..., min_length=5, max_length=100)
password: str = Field(..., min_length=6, max_length=20)

正则表达式验证

1
2
3
4
5
6
7
8
9
10
11
12
from pydantic import BaseModel, Field, validator
import re

class UserCreate(BaseModel):
username: str = Field(..., min_length=3, max_length=20)

@validator('username')
def validate_username(cls, v):
# 只能包含字母、数字、下划线
if not re.match(r'^[a-zA-Z0-9_]+$', v):
raise ValueError('用户名只能包含字母、数字、下划线')
return v

3.3.2 数字验证

范围限制

1
2
3
4
5
6
7
8
9
10
from pydantic import BaseModel, Field

class UserCreate(BaseModel):
age: int = Field(..., ge=0, le=150)
# ge=0:大于等于0
# le=150:小于等于150

score: float = Field(..., gt=0, lt=100)
# gt=0:大于0
# lt=100:小于100

3.3.3 邮箱验证

使用EmailStr

1
2
3
4
from pydantic import BaseModel, EmailStr

class UserCreate(BaseModel):
email: EmailStr # 自动验证邮箱格式

3.3.4 枚举验证

限制可选值

1
2
3
4
5
6
7
8
9
10
11
from pydantic import BaseModel
from enum import Enum

class UserStatus(str, Enum):
ACTIVE = "active" # 激活
INACTIVE = "inactive" # 未激活
DELETED = "deleted" # 已删除

class UserCreate(BaseModel):
name: str
status: UserStatus = UserStatus.ACTIVE # 默认值

3.4 实际例子:用户注册验证

完整的用户注册接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, EmailStr, Field, validator
from typing import Optional
import re

app = FastAPI()

class UserRegister(BaseModel):
username: str = Field(
...,
min_length=3,
max_length=20,
description="用户名,3-20个字符"
)

email: EmailStr = Field(
...,
description="邮箱地址"
)

password: str = Field(
...,
min_length=6,
max_length=20,
description="密码,6-20个字符"
)

age: Optional[int] = Field(
None,
ge=0,
le=150,
description="年龄,0-150"
)

@validator('username')
def validate_username(cls, v):
# 只能包含字母、数字、下划线
if not re.match(r'^[a-zA-Z0-9_]+$', v):
raise ValueError('用户名只能包含字母、数字、下划线')
return v

@validator('password')
def validate_password(cls, v):
# 密码必须包含至少一个数字和一个字母
if not re.search(r'[0-9]', v):
raise ValueError('密码必须包含至少一个数字')
if not re.search(r'[a-zA-Z]', v):
raise ValueError('密码必须包含至少一个字母')
return v

@app.post("/api/users/register")
def register_user(user: UserRegister):
# 数据已经通过验证,可以直接使用
# 检查用户名是否已存在(这里简化处理)
# ...

return {
"code": 200,
"message": "注册成功",
"data": {
"id": 1,
"username": user.username,
"email": user.email
}
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ✅ 正确的数据
{
"username": "zhangsan",
"email": "zhangsan@example.com",
"password": "abc123",
"age": 20
}

# ❌ 错误的数据(会自动返回验证错误)
{
"username": "zh", # 太短,少于3个字符
"email": "not-email", # 不是邮箱格式
"password": "123", # 太短,少于6个字符
"age": 200 # 超过150
}

3.5 验证错误处理

3.5.1 FastAPI自动处理验证错误

当数据验证失败时,FastAPI会自动返回错误信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"detail": [
{
"loc": ["body", "email"],
"msg": "value is not a valid email address",
"type": "value_error.email"
},
{
"loc": ["body", "password"],
"msg": "ensure this value has at least 6 characters",
"type": "value_error.any_str.min_length"
}
]
}

3.5.2 自定义错误信息

使用Field的description和自定义validator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pydantic import BaseModel, Field, validator

class UserCreate(BaseModel):
password: str = Field(
...,
min_length=6,
description="密码,至少6个字符"
)

@validator('password')
def validate_password(cls, v):
if len(v) < 6:
raise ValueError('密码长度至少6个字符,请重新输入')
return v

3.6 实际例子:文章发布验证

文章发布接口的完整验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
from fastapi import FastAPI
from pydantic import BaseModel, Field, validator
from typing import Optional
from datetime import datetime

app = FastAPI()

class ArticleCreate(BaseModel):
title: str = Field(
...,
min_length=1,
max_length=200,
description="文章标题,1-200个字符"
)

content: str = Field(
...,
min_length=10,
description="文章内容,至少10个字符"
)

category: str = Field(
...,
description="文章分类"
)

tags: Optional[list] = Field(
None,
description="标签列表"
)

@validator('title')
def validate_title(cls, v):
# 标题不能全是空格
if v.strip() == "":
raise ValueError('标题不能为空')
return v.strip()

@validator('content')
def validate_content(cls, v):
# 内容不能全是空格
if v.strip() == "":
raise ValueError('内容不能为空')
# 内容不能太短
if len(v.strip()) < 10:
raise ValueError('文章内容至少需要10个字符')
return v

@validator('tags')
def validate_tags(cls, v):
if v is not None:
# 标签数量不能超过10个
if len(v) > 10:
raise ValueError('标签数量不能超过10个')
# 每个标签长度不能超过20个字符
for tag in v:
if len(tag) > 20:
raise ValueError('每个标签长度不能超过20个字符')
return v

@app.post("/api/articles")
def create_article(article: ArticleCreate):
# 数据已经通过验证
return {
"code": 200,
"message": "发布成功",
"data": {
"id": 1,
"title": article.title,
"category": article.category,
"created_at": datetime.now()
}
}

3.7 数据验证最佳实践

验证原则

原则 说明 例子
前端验证 + 后端验证 前端验证提升用户体验,后端验证保证安全 前端提示"密码太短",后端拒绝无效数据
尽早验证 在数据进入业务逻辑前就验证 在接口入口处验证,不要等到数据库操作时
清晰的错误信息 告诉用户哪里错了,怎么改 "密码至少6个字符"比"验证失败"更清晰
验证所有输入 不要相信任何输入,都要验证 即使用户ID也要验证是否存在

实际例子

1
2
3
4
5
6
7
8
9
10
11
# ✅ 好的验证
class UserCreate(BaseModel):
name: str = Field(..., min_length=2, max_length=50, description="姓名,2-50个字符")
email: EmailStr = Field(..., description="邮箱地址")
age: int = Field(..., ge=0, le=150, description="年龄,0-150")

# ❌ 不好的验证
class UserCreate(BaseModel):
name: str # 没有长度限制
email: str # 没有格式验证
age: int # 没有范围限制

💡 关键理解
数据验证是后端安全的第一道防线。
永远不要相信前端传来的数据,必须在后端进行验证!


4. AI实战:使用AI实现Excel导入导出

4.1 任务描述

需求:实现一个Excel文件的导入导出功能

功能要求

  1. 导入功能:上传Excel文件,解析数据并保存到数据库
  2. 导出功能:从数据库查询数据,生成Excel文件并下载
  3. 数据验证:导入时验证数据格式和内容

实际应用场景

  • 批量导入用户数据
  • 导出报表数据
  • 数据备份和恢复

4.2 第一步:设计Prompt

给AI的Prompt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
请帮我实现一个Excel导入导出功能,要求如下:

1. 使用FastAPI框架
2. 使用openpyxl库处理Excel文件
3. 实现用户数据的导入和导出

具体要求:
- 导入:接收Excel文件,解析用户数据(姓名、邮箱、年龄),验证数据,保存到数据库
- 导出:从数据库查询用户数据,生成Excel文件,返回文件下载

数据格式:
- Excel第一行是表头:姓名、邮箱、年龄
- 从第二行开始是数据

请提供完整的代码,包括:
1. 数据模型定义
2. 导入接口实现
3. 导出接口实现
4. 数据验证逻辑

4.3 AI生成的代码(示例)

4.3.1 安装依赖

1
pip install fastapi uvicorn openpyxl python-multipart

4.3.2 完整代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
from fastapi import FastAPI, UploadFile, File, HTTPException
from fastapi.responses import FileResponse
from pydantic import BaseModel, EmailStr, Field, validator
from typing import List, Optional
from openpyxl import Workbook, load_workbook
from openpyxl.styles import Font, Alignment
import io
import re

app = FastAPI()

# 数据模型
class UserImport(BaseModel):
name: str = Field(..., min_length=1, max_length=50)
email: EmailStr
age: int = Field(..., ge=0, le=150)

class UserExport(BaseModel):
id: int
name: str
email: str
age: int

# 模拟数据库(实际项目中应该用真实数据库)
users_db = []
next_id = 1

# Excel导入接口
@app.post("/api/users/import")
async def import_users(file: UploadFile = File(...)):
"""
导入用户数据
接收Excel文件,解析数据,验证并保存
"""
# 检查文件类型
if not file.filename.endswith(('.xlsx', '.xls')):
raise HTTPException(status_code=400, detail="只支持Excel文件(.xlsx, .xls)")

try:
# 读取Excel文件
contents = await file.read()
workbook = load_workbook(io.BytesIO(contents))
sheet = workbook.active

# 验证表头
headers = [cell.value for cell in sheet[1]]
expected_headers = ['姓名', '邮箱', '年龄']
if headers != expected_headers:
raise HTTPException(
status_code=400,
detail=f"表头不正确,应为:{expected_headers}"
)

# 解析数据
imported_users = []
errors = []

for row_idx, row in enumerate(sheet.iter_rows(min_row=2, values_only=True), start=2):
name, email, age = row

# 跳过空行
if not name and not email:
continue

# 验证数据
try:
# 验证姓名
if not name or not isinstance(name, str) or len(name.strip()) == 0:
errors.append(f"第{row_idx}行:姓名为空")
continue

# 验证邮箱
if not email or not isinstance(email, str):
errors.append(f"第{row_idx}行:邮箱格式错误")
continue

# 验证年龄
if age is None:
errors.append(f"第{row_idx}行:年龄为空")
continue

try:
age = int(age)
if age < 0 or age > 150:
errors.append(f"第{row_idx}行:年龄必须在0-150之间")
continue
except (ValueError, TypeError):
errors.append(f"第{row_idx}行:年龄必须是数字")
continue

# 验证邮箱格式
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
if not re.match(email_pattern, email):
errors.append(f"第{row_idx}行:邮箱格式不正确")
continue

# 检查邮箱是否已存在
if any(u["email"] == email for u in users_db):
errors.append(f"第{row_idx}行:邮箱已存在")
continue

# 创建用户
global next_id
new_user = {
"id": next_id,
"name": name.strip(),
"email": email.strip(),
"age": age
}
users_db.append(new_user)
imported_users.append(new_user)
next_id += 1

except Exception as e:
errors.append(f"第{row_idx}行:处理失败 - {str(e)}")

# 返回结果
return {
"code": 200,
"message": f"导入完成,成功{len(imported_users)}条,失败{len(errors)}条",
"data": {
"imported_count": len(imported_users),
"error_count": len(errors),
"errors": errors[:10] # 只返回前10个错误
}
}

except Exception as e:
raise HTTPException(status_code=500, detail=f"文件处理失败:{str(e)}")

# Excel导出接口
@app.get("/api/users/export")
def export_users():
"""
导出用户数据
从数据库查询数据,生成Excel文件
"""
try:
# 创建工作簿
workbook = Workbook()
sheet = workbook.active
sheet.title = "用户数据"

# 设置表头
headers = ['ID', '姓名', '邮箱', '年龄']
for col_idx, header in enumerate(headers, start=1):
cell = sheet.cell(row=1, column=col_idx)
cell.value = header
cell.font = Font(bold=True)
cell.alignment = Alignment(horizontal='center')

# 填充数据
for row_idx, user in enumerate(users_db, start=2):
sheet.cell(row=row_idx, column=1, value=user["id"])
sheet.cell(row=row_idx, column=2, value=user["name"])
sheet.cell(row=row_idx, column=3, value=user["email"])
sheet.cell(row=row_idx, column=4, value=user["age"])

# 调整列宽
sheet.column_dimensions['A'].width = 10
sheet.column_dimensions['B'].width = 20
sheet.column_dimensions['C'].width = 30
sheet.column_dimensions['D'].width = 10

# 保存到内存
output = io.BytesIO()
workbook.save(output)
output.seek(0)

# 返回文件
return FileResponse(
path=None,
filename="用户数据.xlsx",
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
)

except Exception as e:
raise HTTPException(status_code=500, detail=f"导出失败:{str(e)}")

# 获取用户列表(用于测试)
@app.get("/api/users")
def get_users():
return {
"code": 200,
"data": users_db
}

4.3.3 代码解析

核心功能

  1. 导入功能

    • 接收Excel文件上传
    • 使用openpyxl读取Excel
    • 验证表头格式
    • 逐行解析数据并验证
    • 保存到数据库
  2. 导出功能

    • 从数据库查询数据
    • 使用openpyxl创建Excel
    • 设置表头和样式
    • 填充数据
    • 返回文件下载
  3. 数据验证

    • 验证文件类型
    • 验证表头格式
    • 验证每行数据(姓名、邮箱、年龄)
    • 检查重复数据

4.4 测试接口

4.4.1 测试导入功能

使用APIFox测试

1
2
3
4
方法:POST
URL:http://localhost:8000/api/users/import
请求体:form-data
- file: [选择Excel文件]

Excel文件格式

姓名 邮箱 年龄
张三 zhangsan@example.com 20
李四 lisi@example.com 25
王五 wangwu@example.com 30

4.4.2 测试导出功能

1
2
方法:GET
URL:http://localhost:8000/api/users/export

响应:自动下载Excel文件


4.5 优化建议

4.5.1 处理大文件

问题:如果Excel文件很大(几万行),一次性加载到内存会占用很多内存

解决方案:分批处理

1
2
3
4
5
# 分批处理,每次处理1000行
BATCH_SIZE = 1000
for i in range(0, len(rows), BATCH_SIZE):
batch = rows[i:i+BATCH_SIZE]
process_batch(batch)

4.5.2 异步处理

问题:导入大文件时,接口响应时间很长

解决方案:使用后台任务

1
2
3
4
5
6
7
8
9
10
from fastapi import BackgroundTasks

@app.post("/api/users/import")
async def import_users(
file: UploadFile = File(...),
background_tasks: BackgroundTasks = None
):
# 异步处理
background_tasks.add_task(process_import, file)
return {"message": "导入任务已提交,正在处理中"}

4.5.3 错误处理优化

返回详细的错误信息

1
2
3
4
5
# 返回每行的错误详情
errors = [
{"row": 2, "field": "邮箱", "error": "邮箱格式不正确"},
{"row": 3, "field": "年龄", "error": "年龄必须在0-150之间"}
]

💡 关键理解
Excel导入导出是常见的业务需求。
使用AI可以快速生成代码,但需要理解代码逻辑,并根据实际需求进行优化。


5. 选讲:数据库索引优化

5.1 为什么需要索引?

问题:数据库中有100万条用户数据,如何快速找到某个用户?

没有索引的情况

1
2
3
4
5
6
-- 查找邮箱为 "zhangsan@example.com" 的用户
SELECT * FROM users WHERE email = 'zhangsan@example.com';

-- 数据库需要逐行扫描,检查每一行的email字段
-- 最坏情况:需要扫描100万行
-- 时间:可能几秒钟

有索引的情况

1
2
3
4
5
6
7
8
-- 创建索引后
CREATE INDEX idx_email ON users(email);

-- 查找邮箱为 "zhangsan@example.com" 的用户
SELECT * FROM users WHERE email = 'zhangsan@example.com';

-- 数据库使用索引,直接定位到目标行
-- 时间:几毫秒

生活类比

  • 没有索引:在一本没有目录的书中找内容,需要逐页翻找
  • 有索引:使用目录,直接翻到对应页码

5.2 什么是索引?

索引 = 数据库的"目录",帮助快速定位数据

工作原理

  1. 数据库为索引字段创建额外的数据结构(如B+树)
  2. 查询时,先查索引,找到数据位置
  3. 根据位置,直接读取数据

实际例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
用户表(users):
┌────┬──────────┬──────────────┐
│ id │ name │ email │
├────┼──────────┼──────────────┤
│ 1 │ 张三 │ zs@qq.com │
│ 2 │ 李四 │ ls@qq.com │
│ 3 │ 王五 │ ww@qq.com │
└────┴──────────┴──────────────┘

索引(idx_email):
┌──────────────┬──────┐
│ email │ id │
├──────────────┼──────┤
│ ls@qq.com │ 2 │
│ ww@qq.com │ 3 │
│ zs@qq.com │ 1 │
└──────────────┴──────┘

查询 "zs@qq.com":
1. 在索引中查找 "zs@qq.com" → 找到 id=1
2. 根据 id=1,直接读取 users 表的第1行

5.3 索引的类型

5.3.1 主键索引(PRIMARY KEY)

特点

  • ✅ 自动创建(定义主键时自动创建)
  • ✅ 唯一性(不能有重复值)
  • ✅ 非空(不能为NULL)

实际例子

1
2
3
4
CREATE TABLE users (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, -- 自动创建主键索引
name VARCHAR(50)
);

5.3.2 唯一索引(UNIQUE)

特点

  • ✅ 唯一性(不能有重复值)
  • ✅ 可以为NULL(但NULL值只能有一个)

实际例子

1
2
3
4
5
CREATE TABLE users (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(100) UNIQUE, -- 创建唯一索引
username VARCHAR(50) UNIQUE -- 创建唯一索引
);

5.3.3 普通索引(INDEX)

特点

  • ✅ 允许重复值
  • ✅ 提高查询速度

实际例子

1
2
3
4
5
6
7
8
9
-- 创建普通索引
CREATE INDEX idx_name ON users(name);

-- 或者创建表时定义
CREATE TABLE users (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50),
INDEX idx_name(name) -- 创建普通索引
);

5.3.4 组合索引(复合索引)

特点

  • ✅ 多个字段组合成一个索引
  • ✅ 适合多字段查询

实际例子

1
2
3
4
5
6
7
-- 创建组合索引
CREATE INDEX idx_name_age ON users(name, age);

-- 适合的查询:
SELECT * FROM users WHERE name = '张三' AND age = 20; -- ✅ 使用索引
SELECT * FROM users WHERE name = '张三'; -- ✅ 使用索引(最左前缀)
SELECT * FROM users WHERE age = 20; -- ❌ 不使用索引(没有最左字段)

5.4 什么时候需要创建索引?

5.4.1 需要创建索引的场景

场景 说明 例子
主键字段 自动创建,无需手动创建 id INT PRIMARY KEY
唯一字段 经常用于查询和关联 email VARCHAR(100) UNIQUE
外键字段 经常用于关联查询 user_id INT, FOREIGN KEY (user_id) REFERENCES users(id)
经常查询的字段 WHERE条件中经常使用的字段 status TINYINT, INDEX idx_status(status)
经常排序的字段 ORDER BY中经常使用的字段 created_at DATETIME, INDEX idx_created_at(created_at)
经常分组的字段 GROUP BY中经常使用的字段 category VARCHAR(50), INDEX idx_category(category)

5.4.2 不需要创建索引的场景

场景 说明
数据量小的表 少于1000行的表,索引效果不明显
很少查询的字段 几乎不用于查询条件的字段
经常更新的字段 索引需要维护,更新频繁会影响性能
文本字段(过长) TEXT、LONGTEXT等,索引效果差

5.5 索引的优缺点

5.5.1 优点

优点 说明
提高查询速度 查询速度提升几十倍甚至上百倍
加速排序 ORDER BY操作更快
加速关联 JOIN操作更快
保证唯一性 唯一索引保证数据唯一

5.5.2 缺点

缺点 说明
占用存储空间 索引需要额外的存储空间
降低写入速度 INSERT、UPDATE、DELETE时需要更新索引
维护成本 索引需要定期维护

实际例子

1
2
3
4
5
6
7
8
-- 没有索引:插入快,查询慢
INSERT INTO users (name, email) VALUES ('张三', 'zs@qq.com'); -- 快
SELECT * FROM users WHERE email = 'zs@qq.com'; -- 慢(扫描全表)

-- 有索引:插入稍慢,查询快
CREATE INDEX idx_email ON users(email);
INSERT INTO users (name, email) VALUES ('张三', 'zs@qq.com'); -- 稍慢(需要更新索引)
SELECT * FROM users WHERE email = 'zs@qq.com'; -- 快(使用索引)

5.6 实际例子:博客系统索引设计

用户表(users)

1
2
3
4
5
6
7
8
9
10
CREATE TABLE users (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, -- 主键索引(自动)
username VARCHAR(50) NOT NULL UNIQUE, -- 唯一索引(自动)
email VARCHAR(100) NOT NULL UNIQUE, -- 唯一索引(自动)
password VARCHAR(255) NOT NULL,
status TINYINT DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_status(status), -- 普通索引:经常按状态查询
INDEX idx_created_at(created_at) -- 普通索引:经常按时间排序
);

文章表(articles)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CREATE TABLE articles (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, -- 主键索引(自动)
title VARCHAR(200) NOT NULL,
content TEXT NOT NULL,
author_id INT UNSIGNED NOT NULL, -- 外键,需要索引
category VARCHAR(50),
status TINYINT DEFAULT 1,
view_count INT UNSIGNED DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_author_id(author_id), -- 普通索引:经常按作者查询
INDEX idx_category(category), -- 普通索引:经常按分类查询
INDEX idx_status(status), -- 普通索引:经常按状态查询
INDEX idx_created_at(created_at), -- 普通索引:经常按时间排序
INDEX idx_category_status(category, status), -- 组合索引:经常同时按分类和状态查询
FOREIGN KEY (author_id) REFERENCES users(id)
);

评论表(comments)

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE comments (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, -- 主键索引(自动)
article_id INT UNSIGNED NOT NULL, -- 外键,需要索引
user_id INT UNSIGNED NOT NULL, -- 外键,需要索引
content TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_article_id(article_id), -- 普通索引:经常按文章查询
INDEX idx_user_id(user_id), -- 普通索引:经常按用户查询
INDEX idx_article_created(article_id, created_at), -- 组合索引:按文章和时间排序
FOREIGN KEY (article_id) REFERENCES articles(id),
FOREIGN KEY (user_id) REFERENCES users(id)
);

索引使用场景

1
2
3
4
5
6
7
-- ✅ 使用索引的查询
SELECT * FROM articles WHERE author_id = 1; -- 使用 idx_author_id
SELECT * FROM articles WHERE category = '技术' AND status = 1; -- 使用 idx_category_status
SELECT * FROM comments WHERE article_id = 1 ORDER BY created_at; -- 使用 idx_article_created

-- ❌ 不使用索引的查询
SELECT * FROM articles WHERE content LIKE '%关键词%'; -- TEXT字段,无法使用索引

5.7 索引优化技巧

5.7.1 最左前缀原则

组合索引遵循最左前缀原则

1
2
3
4
5
6
7
8
9
10
11
12
-- 创建组合索引
CREATE INDEX idx_name_age_city ON users(name, age, city);

-- ✅ 可以使用索引的查询
SELECT * FROM users WHERE name = '张三'; -- 使用索引
SELECT * FROM users WHERE name = '张三' AND age = 20; -- 使用索引
SELECT * FROM users WHERE name = '张三' AND age = 20 AND city = '北京'; -- 使用索引

-- ❌ 不能使用索引的查询
SELECT * FROM users WHERE age = 20; -- 没有最左字段name
SELECT * FROM users WHERE city = '北京'; -- 没有最左字段name
SELECT * FROM users WHERE age = 20 AND city = '北京'; -- 没有最左字段name

5.7.2 避免过多索引

问题:索引不是越多越好

原因

  • ❌ 每个索引都需要存储空间
  • ❌ 每次INSERT/UPDATE/DELETE都需要更新所有相关索引
  • ❌ 索引过多会影响写入性能

建议

  • ✅ 只为经常查询的字段创建索引
  • ✅ 定期检查未使用的索引并删除

5.7.3 索引字段选择

选择原则

字段类型 是否适合索引 说明
整数类型 ✅ 非常适合 INT、TINYINT等,查询效率高
短字符串 ✅ 适合 VARCHAR(50)以内,可以创建索引
长字符串 ⚠️ 谨慎使用 VARCHAR(255)以上,索引效果差
文本类型 ❌ 不适合 TEXT、LONGTEXT,无法创建普通索引
日期时间 ✅ 适合 DATETIME、TIMESTAMP,经常用于排序

5.8 查看索引使用情况

5.8.1 查看表的索引

1
2
3
4
5
6
7
-- 查看表的所有索引
SHOW INDEX FROM users;

-- 结果示例:
-- Key_name: PRIMARY, Column_name: id
-- Key_name: idx_email, Column_name: email
-- Key_name: idx_status, Column_name: status

5.8.2 分析查询是否使用索引

1
2
3
4
5
6
-- 使用EXPLAIN分析查询
EXPLAIN SELECT * FROM users WHERE email = 'zhangsan@example.com';

-- 结果中的key字段显示使用的索引
-- key: idx_email 表示使用了idx_email索引
-- key: NULL 表示没有使用索引

5.9 索引优化总结

创建索引的原则

  1. 主键和外键:必须创建索引
  2. 经常查询的字段:创建索引
  3. 经常排序的字段:创建索引
  4. 组合查询:创建组合索引
  5. 不要过度索引:只为必要的字段创建索引

索引优化检查清单

  • ✅ 主键字段有索引
  • ✅ 外键字段有索引
  • ✅ 唯一字段有索引
  • ✅ 经常WHERE的字段有索引
  • ✅ 经常ORDER BY的字段有索引
  • ✅ 组合查询有组合索引
  • ❌ 没有为很少查询的字段创建索引
  • ❌ 没有为TEXT字段创建普通索引

💡 关键理解
索引是数据库性能优化的关键。
合理使用索引可以大幅提升查询速度,但也要注意不要过度索引。
对于初学者,先为主键、外键、唯一字段创建索引,再根据实际查询需求添加其他索引。


本章总结

通过本章的学习,你已经掌握了:

数据库设计

  • MySQL数据类型的选择(TINYINT、VARCHAR、TEXT等)
  • 根据实际需求选择合适的数据类型
  • 节省存储空间,提高查询性能

API实现

  • 使用FastAPI实现CRUD接口
  • 连接数据库进行数据操作
  • 统一响应格式和错误处理

数据验证

  • 使用Pydantic进行数据验证
  • 常用验证规则(字符串、数字、邮箱等)
  • 验证错误处理

实战技能

  • 使用AI实现Excel导入导出功能
  • 处理文件上传和下载
  • 数据验证和错误处理

进阶知识(选讲):

  • 数据库索引的作用和类型
  • 索引的创建和使用
  • 索引优化技巧

🌟 恭喜你!
你已经掌握了后端开发的基础技能!
这些知识将帮助你:

  • 设计合理的数据库结构

  • 实现规范的API接口

  • 处理数据验证和错误

  • 优化数据库查询性能

记住:后端开发的核心是数据处理和业务逻辑!
继续练习,继续探索,后端开发的世界等着你! 🚀