👋 我是 Yulia,一线 AI 应用开发工程师:4 年工程经验,在职做大模型应用落地。
只写一件事------把最新 AI 技术讲成人话 + 给你能直接上手用的代码 (真实可验证,不搬运不注水)。
有用就点个 关注,持续更新大模型应用的前沿实战。
📚 本文是《后端开发从入门到靠谱》系列第 8 篇。第七课我们用 FastAPI 写了一个 AI 问答接口,但它有个致命缺陷------一问一答后什么都没留下,服务一重启,历史全没了。这一课补上"记忆":给接口接上数据库,把问答历史存下来。整个系列共 20 篇,从 0 基础一路到能独立写生产级后端。
上一课的 /ask 接口能问答,但你有没有发现一个问题:你问过什么、答过什么,它全忘了 。因为之前我们只是把答案"返回"给客户端,数据存在内存里------服务一重启,内存清空,什么都没了。
真实的应用(客服机器人、AI 助手)一定会有"历史记录""对话上下文"。要做到这些,就需要数据库------给程序装上"记忆"。
读完你会拿到:
- 一个直觉模型:内存 vs 数据库的本质差别(白板 vs 笔记本类比)
- 数据库的两大门派:SQL 关系型 vs NoSQL(Excel vs 文件夹类比,含选型)
- 关系型数据库的核心概念:表 / 行 / 列 / 主键 / 外键(0 基础也能懂)
- SQLite 入门(Python 内置,零安装,代码可直接跑)
- ORM 是什么、为什么能少写一半 SQL(翻译官类比 + SQLAlchemy)
- 一个完整实战:把问答历史存进数据库(衔接上一课 FastAPI 接口)
- 一个安全必修课:SQL 注入是怎么发生的、怎么防(参数化查询)
先看全景图,建立全局认知:
#mermaid-svg-PrlNnxVpNILsBbuD{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-PrlNnxVpNILsBbuD .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-PrlNnxVpNILsBbuD .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-PrlNnxVpNILsBbuD .error-icon{fill:#552222;}#mermaid-svg-PrlNnxVpNILsBbuD .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-PrlNnxVpNILsBbuD .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-PrlNnxVpNILsBbuD .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-PrlNnxVpNILsBbuD .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-PrlNnxVpNILsBbuD .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-PrlNnxVpNILsBbuD .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-PrlNnxVpNILsBbuD .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-PrlNnxVpNILsBbuD .marker{fill:#333333;stroke:#333333;}#mermaid-svg-PrlNnxVpNILsBbuD .marker.cross{stroke:#333333;}#mermaid-svg-PrlNnxVpNILsBbuD svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-PrlNnxVpNILsBbuD p{margin:0;}#mermaid-svg-PrlNnxVpNILsBbuD .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-PrlNnxVpNILsBbuD .cluster-label text{fill:#333;}#mermaid-svg-PrlNnxVpNILsBbuD .cluster-label span{color:#333;}#mermaid-svg-PrlNnxVpNILsBbuD .cluster-label span p{background-color:transparent;}#mermaid-svg-PrlNnxVpNILsBbuD .label text,#mermaid-svg-PrlNnxVpNILsBbuD span{fill:#333;color:#333;}#mermaid-svg-PrlNnxVpNILsBbuD .node rect,#mermaid-svg-PrlNnxVpNILsBbuD .node circle,#mermaid-svg-PrlNnxVpNILsBbuD .node ellipse,#mermaid-svg-PrlNnxVpNILsBbuD .node polygon,#mermaid-svg-PrlNnxVpNILsBbuD .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-PrlNnxVpNILsBbuD .rough-node .label text,#mermaid-svg-PrlNnxVpNILsBbuD .node .label text,#mermaid-svg-PrlNnxVpNILsBbuD .image-shape .label,#mermaid-svg-PrlNnxVpNILsBbuD .icon-shape .label{text-anchor:middle;}#mermaid-svg-PrlNnxVpNILsBbuD .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-PrlNnxVpNILsBbuD .rough-node .label,#mermaid-svg-PrlNnxVpNILsBbuD .node .label,#mermaid-svg-PrlNnxVpNILsBbuD .image-shape .label,#mermaid-svg-PrlNnxVpNILsBbuD .icon-shape .label{text-align:center;}#mermaid-svg-PrlNnxVpNILsBbuD .node.clickable{cursor:pointer;}#mermaid-svg-PrlNnxVpNILsBbuD .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-PrlNnxVpNILsBbuD .arrowheadPath{fill:#333333;}#mermaid-svg-PrlNnxVpNILsBbuD .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-PrlNnxVpNILsBbuD .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-PrlNnxVpNILsBbuD .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PrlNnxVpNILsBbuD .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-PrlNnxVpNILsBbuD .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PrlNnxVpNILsBbuD .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-PrlNnxVpNILsBbuD .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-PrlNnxVpNILsBbuD .cluster text{fill:#333;}#mermaid-svg-PrlNnxVpNILsBbuD .cluster span{color:#333;}#mermaid-svg-PrlNnxVpNILsBbuD div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-PrlNnxVpNILsBbuD .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-PrlNnxVpNILsBbuD rect.text{fill:none;stroke-width:0;}#mermaid-svg-PrlNnxVpNILsBbuD .icon-shape,#mermaid-svg-PrlNnxVpNILsBbuD .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PrlNnxVpNILsBbuD .icon-shape p,#mermaid-svg-PrlNnxVpNILsBbuD .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-PrlNnxVpNILsBbuD .icon-shape .label rect,#mermaid-svg-PrlNnxVpNILsBbuD .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PrlNnxVpNILsBbuD .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-PrlNnxVpNILsBbuD .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-PrlNnxVpNILsBbuD :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-PrlNnxVpNILsBbuD .blue>*{fill:#4E79A7!important;color:#fff!important;stroke:none!important;}#mermaid-svg-PrlNnxVpNILsBbuD .blue span{fill:#4E79A7!important;color:#fff!important;stroke:none!important;}#mermaid-svg-PrlNnxVpNILsBbuD .blue tspan{fill:#fff!important;}#mermaid-svg-PrlNnxVpNILsBbuD .orange>*{fill:#F28E2B!important;color:#fff!important;stroke:none!important;}#mermaid-svg-PrlNnxVpNILsBbuD .orange span{fill:#F28E2B!important;color:#fff!important;stroke:none!important;}#mermaid-svg-PrlNnxVpNILsBbuD .orange tspan{fill:#fff!important;}#mermaid-svg-PrlNnxVpNILsBbuD .green>*{fill:#59A14F!important;color:#fff!important;stroke:none!important;}#mermaid-svg-PrlNnxVpNILsBbuD .green span{fill:#59A14F!important;color:#fff!important;stroke:none!important;}#mermaid-svg-PrlNnxVpNILsBbuD .green tspan{fill:#fff!important;}#mermaid-svg-PrlNnxVpNILsBbuD .red>*{fill:#E15759!important;color:#fff!important;stroke:none!important;}#mermaid-svg-PrlNnxVpNILsBbuD .red span{fill:#E15759!important;color:#fff!important;stroke:none!important;}#mermaid-svg-PrlNnxVpNILsBbuD .red tspan{fill:#fff!important;}#mermaid-svg-PrlNnxVpNILsBbuD .cyan>*{fill:#76B7B2!important;color:#fff!important;stroke:none!important;}#mermaid-svg-PrlNnxVpNILsBbuD .cyan span{fill:#76B7B2!important;color:#fff!important;stroke:none!important;}#mermaid-svg-PrlNnxVpNILsBbuD .cyan tspan{fill:#fff!important;} 存问答
翻译成 SQL
写入
查询
返回对象
返回数据
FastAPI 接口
SQLAlchemy ORM
SQLite 数据库
磁盘文件 chat.db
一、为什么需要数据库:内存会"失忆"
先搞懂最核心的问题:为什么不能把数据一直放在变量里?
一句话:内存是"白板",数据库是"笔记本"。
#mermaid-svg-LIvEXUQ5EIgAVJfO{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-LIvEXUQ5EIgAVJfO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-LIvEXUQ5EIgAVJfO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-LIvEXUQ5EIgAVJfO .error-icon{fill:#552222;}#mermaid-svg-LIvEXUQ5EIgAVJfO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-LIvEXUQ5EIgAVJfO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-LIvEXUQ5EIgAVJfO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-LIvEXUQ5EIgAVJfO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-LIvEXUQ5EIgAVJfO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-LIvEXUQ5EIgAVJfO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-LIvEXUQ5EIgAVJfO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-LIvEXUQ5EIgAVJfO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-LIvEXUQ5EIgAVJfO .marker.cross{stroke:#333333;}#mermaid-svg-LIvEXUQ5EIgAVJfO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-LIvEXUQ5EIgAVJfO p{margin:0;}#mermaid-svg-LIvEXUQ5EIgAVJfO .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-LIvEXUQ5EIgAVJfO .cluster-label text{fill:#333;}#mermaid-svg-LIvEXUQ5EIgAVJfO .cluster-label span{color:#333;}#mermaid-svg-LIvEXUQ5EIgAVJfO .cluster-label span p{background-color:transparent;}#mermaid-svg-LIvEXUQ5EIgAVJfO .label text,#mermaid-svg-LIvEXUQ5EIgAVJfO span{fill:#333;color:#333;}#mermaid-svg-LIvEXUQ5EIgAVJfO .node rect,#mermaid-svg-LIvEXUQ5EIgAVJfO .node circle,#mermaid-svg-LIvEXUQ5EIgAVJfO .node ellipse,#mermaid-svg-LIvEXUQ5EIgAVJfO .node polygon,#mermaid-svg-LIvEXUQ5EIgAVJfO .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-LIvEXUQ5EIgAVJfO .rough-node .label text,#mermaid-svg-LIvEXUQ5EIgAVJfO .node .label text,#mermaid-svg-LIvEXUQ5EIgAVJfO .image-shape .label,#mermaid-svg-LIvEXUQ5EIgAVJfO .icon-shape .label{text-anchor:middle;}#mermaid-svg-LIvEXUQ5EIgAVJfO .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-LIvEXUQ5EIgAVJfO .rough-node .label,#mermaid-svg-LIvEXUQ5EIgAVJfO .node .label,#mermaid-svg-LIvEXUQ5EIgAVJfO .image-shape .label,#mermaid-svg-LIvEXUQ5EIgAVJfO .icon-shape .label{text-align:center;}#mermaid-svg-LIvEXUQ5EIgAVJfO .node.clickable{cursor:pointer;}#mermaid-svg-LIvEXUQ5EIgAVJfO .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-LIvEXUQ5EIgAVJfO .arrowheadPath{fill:#333333;}#mermaid-svg-LIvEXUQ5EIgAVJfO .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-LIvEXUQ5EIgAVJfO .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-LIvEXUQ5EIgAVJfO .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LIvEXUQ5EIgAVJfO .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-LIvEXUQ5EIgAVJfO .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LIvEXUQ5EIgAVJfO .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-LIvEXUQ5EIgAVJfO .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-LIvEXUQ5EIgAVJfO .cluster text{fill:#333;}#mermaid-svg-LIvEXUQ5EIgAVJfO .cluster span{color:#333;}#mermaid-svg-LIvEXUQ5EIgAVJfO div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-LIvEXUQ5EIgAVJfO .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-LIvEXUQ5EIgAVJfO rect.text{fill:none;stroke-width:0;}#mermaid-svg-LIvEXUQ5EIgAVJfO .icon-shape,#mermaid-svg-LIvEXUQ5EIgAVJfO .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LIvEXUQ5EIgAVJfO .icon-shape p,#mermaid-svg-LIvEXUQ5EIgAVJfO .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-LIvEXUQ5EIgAVJfO .icon-shape .label rect,#mermaid-svg-LIvEXUQ5EIgAVJfO .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LIvEXUQ5EIgAVJfO .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-LIvEXUQ5EIgAVJfO .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-LIvEXUQ5EIgAVJfO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-LIvEXUQ5EIgAVJfO .blue>*{fill:#4E79A7!important;color:#fff!important;stroke:none!important;}#mermaid-svg-LIvEXUQ5EIgAVJfO .blue span{fill:#4E79A7!important;color:#fff!important;stroke:none!important;}#mermaid-svg-LIvEXUQ5EIgAVJfO .blue tspan{fill:#fff!important;}#mermaid-svg-LIvEXUQ5EIgAVJfO .orange>*{fill:#F28E2B!important;color:#fff!important;stroke:none!important;}#mermaid-svg-LIvEXUQ5EIgAVJfO .orange span{fill:#F28E2B!important;color:#fff!important;stroke:none!important;}#mermaid-svg-LIvEXUQ5EIgAVJfO .orange tspan{fill:#fff!important;} 程序一关 全部消失
写到磁盘 永久保存
内存里的变量
白板 一擦就没
数据库
笔记本 一直留着
你在 Python 里写 history = [],把问答记录 append 进去------这些数据存在**内存(RAM)**里。内存的特点是:
- 快:读写速度极快(纳秒级);
- 易失:断电、程序退出,数据立刻消失。
而数据库把数据写到磁盘(硬盘/SSD)上,即使服务器重启、断电,数据还在。这就是持久化(persistence)------让数据"活"过这一次运行。
为什么不全用内存?因为内存贵、容量小、且一断电就没了。磁盘便宜、容量大、断电也不丢,代价是读写慢一些。数据库就是"牺牲一点速度,换永久保存"的折中方案。
二、数据库两大门派:SQL 关系型 vs NoSQL
数据库不是一种,而是两大类。选错类型,后面全别扭。
| SQL 关系型 | NoSQL 非关系型 | |
|---|---|---|
| 类比 | Excel 表格(规规矩矩的行列) | 文件夹(每个文件自己管自己) |
| 代表 | MySQL、PostgreSQL、SQLite | MongoDB、Redis |
| 数据结构 | 表(table),行列固定 | 文档/键值对,结构灵活 |
| 适合 | 关系明确、要一致性(订单、用户) | 结构多变、要快(缓存、日志) |
| 查询语言 | SQL(标准统一) | 各自不同 |
关系型(SQL) 就像 Excel:每个表有固定的列(字段),每行是一条记录,表与表之间还能"关联"(外键)。它强在数据规整、一致性强、查询语言统一------这是后端最主流的默认选择。
非关系型(NoSQL) 更像文件夹:每条数据可以是任意结构,没有固定表格。它强在灵活、水平扩展容易、某些场景极快。
这一课我们学关系型,因为它是后端基础,且 AI 应用里存对话历史、用户数据这类"关系明确"的数据,默认就用关系型。NoSQL(尤其 Redis 做缓存)后续再讲。
三、关系型数据库的核心概念:表 / 行 / 列 / 键
用一张"用户表"建立直觉:
| id(主键) | name | age | city |
|---|---|---|---|
| 1 | 张三 | 25 | 北京 |
| 2 | 李四 | 30 | 上海 |
- 表(table):一整张 Excel 表格,比如"用户表"。
- 列(column)/字段 :表的一竖列,比如
name、age,每个字段有固定类型(整数、字符串等)。 - 行(row)/记录:表的一横行,一条完整数据("张三,25 岁,北京")。
- 主键(primary key) :唯一标识一行的那一列,通常是
id。任何表都必须有主键,就像每个人有身份证号。 - 外键(foreign key) :一列引用另一张表的主键,用来建立"表与表的关系"。比如"订单表"里有个
user_id列,指向"用户表"的id------表示"这笔订单是谁下的"。
用 SQL 语言操作这张表,只有四种基本动作,记住就够入门:
| 动作 | SQL | 人话 |
|---|---|---|
| 查 | SELECT * FROM users WHERE age > 20 |
找出年龄大于 20 的用户 |
| 增 | INSERT INTO users (name, age) VALUES ('王五', 28) |
插入一条新用户 |
| 改 | UPDATE users SET city = '深圳' WHERE id = 1 |
把 id=1 的城市改成深圳 |
| 删 | DELETE FROM users WHERE id = 2 |
删掉 id=2 的用户 |
这四个词(增删改查,合称 CRUD)覆盖了后端 90% 的数据操作。别被 SQL 吓到,它就是"用英文描述你要对表格做什么"。
四、SQLite 入门:Python 内置,零安装
学数据库最烦的是"先装个 MySQL、配一堆东西"。但 Python 内置了 sqlite3 ------一个零配置、零安装的关系型数据库,数据就存在一个 .db 文件里。学习阶段用它最省事。
python
import sqlite3
# 连接数据库(文件不存在会自动创建)
conn = sqlite3.connect("chat.db")
cur = conn.cursor()
# 建一张表
cur.execute("""
CREATE TABLE IF NOT EXISTS chat_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
question TEXT NOT NULL,
answer TEXT NOT NULL
)
""")
# 插入一条记录(用 ? 占位符,防 SQL 注入,后面专门讲)
cur.execute("INSERT INTO chat_records (question, answer) VALUES (?, ?)",
("什么是 FastAPI?", "一个现代 Python Web 框架"))
# 查询所有记录
cur.execute("SELECT * FROM chat_records")
for row in cur.fetchall():
print(row)
conn.commit() # 提交(真正写入磁盘)
conn.close() # 关闭连接
逐行讲清关键点:
sqlite3.connect("chat.db")------连接数据库,本质是打开(或创建)一个叫chat.db的文件。你的整个数据库,就是一个文件。CREATE TABLE IF NOT EXISTS------建表语句,id INTEGER PRIMARY KEY AUTOINCREMENT意思是"id 是主键、自动递增"。?占位符------不要用字符串拼接 SQL ,用?占位 + 参数元组,这是防 SQL 注入的关键(第六节专门讲为什么)。conn.commit()------SQLite 的增删改默认不立刻写磁盘,要commit(提交)才真正生效。
五、ORM:用 Python 对象操作数据库,少写一半 SQL
上面直接写 SQL 虽然能用,但有三个痛点:① 容易拼错 SQL 字符串;② 结果是一堆裸数据,不是 Python 对象;③ 换数据库(SQLite → MySQL)时 SQL 语法可能有差异要重写。
ORM(Object-Relational Mapping,对象关系映射) 就是来解决这些的。一句话:ORM 是"翻译官",让你用 Python 类来操作数据库,它帮你翻译成 SQL。
#mermaid-svg-M9spJmawphAPb8fS{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-M9spJmawphAPb8fS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-M9spJmawphAPb8fS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-M9spJmawphAPb8fS .error-icon{fill:#552222;}#mermaid-svg-M9spJmawphAPb8fS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-M9spJmawphAPb8fS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-M9spJmawphAPb8fS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-M9spJmawphAPb8fS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-M9spJmawphAPb8fS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-M9spJmawphAPb8fS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-M9spJmawphAPb8fS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-M9spJmawphAPb8fS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-M9spJmawphAPb8fS .marker.cross{stroke:#333333;}#mermaid-svg-M9spJmawphAPb8fS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-M9spJmawphAPb8fS p{margin:0;}#mermaid-svg-M9spJmawphAPb8fS .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-M9spJmawphAPb8fS .cluster-label text{fill:#333;}#mermaid-svg-M9spJmawphAPb8fS .cluster-label span{color:#333;}#mermaid-svg-M9spJmawphAPb8fS .cluster-label span p{background-color:transparent;}#mermaid-svg-M9spJmawphAPb8fS .label text,#mermaid-svg-M9spJmawphAPb8fS span{fill:#333;color:#333;}#mermaid-svg-M9spJmawphAPb8fS .node rect,#mermaid-svg-M9spJmawphAPb8fS .node circle,#mermaid-svg-M9spJmawphAPb8fS .node ellipse,#mermaid-svg-M9spJmawphAPb8fS .node polygon,#mermaid-svg-M9spJmawphAPb8fS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-M9spJmawphAPb8fS .rough-node .label text,#mermaid-svg-M9spJmawphAPb8fS .node .label text,#mermaid-svg-M9spJmawphAPb8fS .image-shape .label,#mermaid-svg-M9spJmawphAPb8fS .icon-shape .label{text-anchor:middle;}#mermaid-svg-M9spJmawphAPb8fS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-M9spJmawphAPb8fS .rough-node .label,#mermaid-svg-M9spJmawphAPb8fS .node .label,#mermaid-svg-M9spJmawphAPb8fS .image-shape .label,#mermaid-svg-M9spJmawphAPb8fS .icon-shape .label{text-align:center;}#mermaid-svg-M9spJmawphAPb8fS .node.clickable{cursor:pointer;}#mermaid-svg-M9spJmawphAPb8fS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-M9spJmawphAPb8fS .arrowheadPath{fill:#333333;}#mermaid-svg-M9spJmawphAPb8fS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-M9spJmawphAPb8fS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-M9spJmawphAPb8fS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-M9spJmawphAPb8fS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-M9spJmawphAPb8fS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-M9spJmawphAPb8fS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-M9spJmawphAPb8fS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-M9spJmawphAPb8fS .cluster text{fill:#333;}#mermaid-svg-M9spJmawphAPb8fS .cluster span{color:#333;}#mermaid-svg-M9spJmawphAPb8fS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-M9spJmawphAPb8fS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-M9spJmawphAPb8fS rect.text{fill:none;stroke-width:0;}#mermaid-svg-M9spJmawphAPb8fS .icon-shape,#mermaid-svg-M9spJmawphAPb8fS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-M9spJmawphAPb8fS .icon-shape p,#mermaid-svg-M9spJmawphAPb8fS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-M9spJmawphAPb8fS .icon-shape .label rect,#mermaid-svg-M9spJmawphAPb8fS .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-M9spJmawphAPb8fS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-M9spJmawphAPb8fS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-M9spJmawphAPb8fS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-M9spJmawphAPb8fS .blue>*{fill:#4E79A7!important;color:#fff!important;stroke:none!important;}#mermaid-svg-M9spJmawphAPb8fS .blue span{fill:#4E79A7!important;color:#fff!important;stroke:none!important;}#mermaid-svg-M9spJmawphAPb8fS .blue tspan{fill:#fff!important;}#mermaid-svg-M9spJmawphAPb8fS .orange>*{fill:#F28E2B!important;color:#fff!important;stroke:none!important;}#mermaid-svg-M9spJmawphAPb8fS .orange span{fill:#F28E2B!important;color:#fff!important;stroke:none!important;}#mermaid-svg-M9spJmawphAPb8fS .orange tspan{fill:#fff!important;}#mermaid-svg-M9spJmawphAPb8fS .green>*{fill:#59A14F!important;color:#fff!important;stroke:none!important;}#mermaid-svg-M9spJmawphAPb8fS .green span{fill:#59A14F!important;color:#fff!important;stroke:none!important;}#mermaid-svg-M9spJmawphAPb8fS .green tspan{fill:#fff!important;} ORM 翻译
ORM 翻译回来
Python 类 对象
SQL 语句
数据库
Python 生态最主流的 ORM 是 SQLAlchemy。上一课的 SQL 写法,用 SQLAlchemy 变成这样:
python
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import declarative_base, sessionmaker
# 1. 连数据库(URL 里写 SQLite,换成 MySQL 也基本不动别的)
engine = create_engine("sqlite:///chat.db")
Base = declarative_base()
# 2. 一个 Python 类 = 一张表
class ChatRecord(Base):
__tablename__ = "chat_records" # 表名
id = Column(Integer, primary_key=True) # 主键
question = Column(String)
answer = Column(String)
# 3. 真正在数据库里建表
Base.metadata.create_all(engine)
# 4. 会话(Session)= 和数据库对话的"通道"
Session = sessionmaker(bind=engine)
session = Session()
# 5. 增:new 一个对象,add + commit
record = ChatRecord(question="什么是 FastAPI?", answer="一个现代 Python Web 框架")
session.add(record)
session.commit()
# 6. 查:直接用类查询,拿到的是 Python 对象列表
records = session.query(ChatRecord).all()
for r in records:
print(r.id, r.question, r.answer)
ORM 的核心洞察 :ChatRecord 这个 Python 类,和数据库里的 chat_records 表是"一一对应"的------类的每个字段对应表的每一列,类的每个实例对应表的一行。你操作 Python 对象,ORM 负责把它翻译成 SQL 执行,再把结果翻译回 Python 对象。你几乎不用手写 SQL 了。
换数据库时,只要把
create_engine("sqlite:///chat.db")里的 URL 换成mysql+pymysql://user:pass@host/db,其余代码基本不变------这就是 ORM 的"数据库无关性"红利。
六、完整实战:把问答历史存进 FastAPI
现在把 ORM 接进上一课的 FastAPI 接口,让它"记住"每次问答。
python
from fastapi import FastAPI, Depends
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import declarative_base, sessionmaker, Session
from pydantic import BaseModel
# ---- 数据库部分(和上一节一样)----
engine = create_engine("sqlite:///chat.db")
Base = declarative_base()
class ChatRecord(Base):
__tablename__ = "chat_records"
id = Column(Integer, primary_key=True)
question = Column(String)
answer = Column(String)
Base.metadata.create_all(engine)
SessionLocal = sessionmaker(bind=engine)
# ---- FastAPI 部分 ----
app = FastAPI()
class Question(BaseModel):
text: str
# 依赖注入:每个请求拿到一个数据库会话,用完自动关闭
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@app.post("/ask")
def ask(q: Question, db: Session = Depends(get_db)):
answer = f"你问的是:{q.text}" # 真实项目里这里调大模型
record = ChatRecord(question=q.text, answer=answer) # 存历史
db.add(record)
db.commit()
return {"question": q.text, "answer": answer}
@app.get("/history")
def history(db: Session = Depends(get_db)):
records = db.query(ChatRecord).order_by(ChatRecord.id.desc()).all() # 按时间倒序
return [{"id": r.id, "question": r.question, "answer": r.answer} for r in records]
逐行讲清新出现的两个概念:
get_db+Depends(依赖注入) :这是 FastAPI 管理数据库会话的标准写法。Depends(get_db)的意思是"这个db参数不要调用方传,由 FastAPI 自动调get_db生成"。yield db在请求处理完后自动执行finally里的db.close()------每个请求用完就关连接,不泄漏。order_by(ChatRecord.id.desc()):按 id 倒序排列,最新的问答排最前------查"最近聊天记录"就靠它。
现在接口有了"记忆":POST /ask 会存一条记录,GET /history 能查回所有历史。重启服务、重启电脑,数据都还在 chat.db 文件里。
#mermaid-svg-Kzc8GtzDIlPDLbMY{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Kzc8GtzDIlPDLbMY .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Kzc8GtzDIlPDLbMY .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Kzc8GtzDIlPDLbMY .error-icon{fill:#552222;}#mermaid-svg-Kzc8GtzDIlPDLbMY .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Kzc8GtzDIlPDLbMY .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Kzc8GtzDIlPDLbMY .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Kzc8GtzDIlPDLbMY .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Kzc8GtzDIlPDLbMY .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Kzc8GtzDIlPDLbMY .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Kzc8GtzDIlPDLbMY .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Kzc8GtzDIlPDLbMY .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Kzc8GtzDIlPDLbMY .marker.cross{stroke:#333333;}#mermaid-svg-Kzc8GtzDIlPDLbMY svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Kzc8GtzDIlPDLbMY p{margin:0;}#mermaid-svg-Kzc8GtzDIlPDLbMY .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Kzc8GtzDIlPDLbMY .cluster-label text{fill:#333;}#mermaid-svg-Kzc8GtzDIlPDLbMY .cluster-label span{color:#333;}#mermaid-svg-Kzc8GtzDIlPDLbMY .cluster-label span p{background-color:transparent;}#mermaid-svg-Kzc8GtzDIlPDLbMY .label text,#mermaid-svg-Kzc8GtzDIlPDLbMY span{fill:#333;color:#333;}#mermaid-svg-Kzc8GtzDIlPDLbMY .node rect,#mermaid-svg-Kzc8GtzDIlPDLbMY .node circle,#mermaid-svg-Kzc8GtzDIlPDLbMY .node ellipse,#mermaid-svg-Kzc8GtzDIlPDLbMY .node polygon,#mermaid-svg-Kzc8GtzDIlPDLbMY .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Kzc8GtzDIlPDLbMY .rough-node .label text,#mermaid-svg-Kzc8GtzDIlPDLbMY .node .label text,#mermaid-svg-Kzc8GtzDIlPDLbMY .image-shape .label,#mermaid-svg-Kzc8GtzDIlPDLbMY .icon-shape .label{text-anchor:middle;}#mermaid-svg-Kzc8GtzDIlPDLbMY .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Kzc8GtzDIlPDLbMY .rough-node .label,#mermaid-svg-Kzc8GtzDIlPDLbMY .node .label,#mermaid-svg-Kzc8GtzDIlPDLbMY .image-shape .label,#mermaid-svg-Kzc8GtzDIlPDLbMY .icon-shape .label{text-align:center;}#mermaid-svg-Kzc8GtzDIlPDLbMY .node.clickable{cursor:pointer;}#mermaid-svg-Kzc8GtzDIlPDLbMY .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Kzc8GtzDIlPDLbMY .arrowheadPath{fill:#333333;}#mermaid-svg-Kzc8GtzDIlPDLbMY .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Kzc8GtzDIlPDLbMY .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Kzc8GtzDIlPDLbMY .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Kzc8GtzDIlPDLbMY .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Kzc8GtzDIlPDLbMY .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Kzc8GtzDIlPDLbMY .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Kzc8GtzDIlPDLbMY .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Kzc8GtzDIlPDLbMY .cluster text{fill:#333;}#mermaid-svg-Kzc8GtzDIlPDLbMY .cluster span{color:#333;}#mermaid-svg-Kzc8GtzDIlPDLbMY div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Kzc8GtzDIlPDLbMY .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Kzc8GtzDIlPDLbMY rect.text{fill:none;stroke-width:0;}#mermaid-svg-Kzc8GtzDIlPDLbMY .icon-shape,#mermaid-svg-Kzc8GtzDIlPDLbMY .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Kzc8GtzDIlPDLbMY .icon-shape p,#mermaid-svg-Kzc8GtzDIlPDLbMY .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Kzc8GtzDIlPDLbMY .icon-shape .label rect,#mermaid-svg-Kzc8GtzDIlPDLbMY .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Kzc8GtzDIlPDLbMY .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Kzc8GtzDIlPDLbMY .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Kzc8GtzDIlPDLbMY :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-Kzc8GtzDIlPDLbMY .blue>*{fill:#4E79A7!important;color:#fff!important;stroke:none!important;}#mermaid-svg-Kzc8GtzDIlPDLbMY .blue span{fill:#4E79A7!important;color:#fff!important;stroke:none!important;}#mermaid-svg-Kzc8GtzDIlPDLbMY .blue tspan{fill:#fff!important;}#mermaid-svg-Kzc8GtzDIlPDLbMY .orange>*{fill:#F28E2B!important;color:#fff!important;stroke:none!important;}#mermaid-svg-Kzc8GtzDIlPDLbMY .orange span{fill:#F28E2B!important;color:#fff!important;stroke:none!important;}#mermaid-svg-Kzc8GtzDIlPDLbMY .orange tspan{fill:#fff!important;}#mermaid-svg-Kzc8GtzDIlPDLbMY .green>*{fill:#59A14F!important;color:#fff!important;stroke:none!important;}#mermaid-svg-Kzc8GtzDIlPDLbMY .green span{fill:#59A14F!important;color:#fff!important;stroke:none!important;}#mermaid-svg-Kzc8GtzDIlPDLbMY .green tspan{fill:#fff!important;} 存一条
查询
返回记录列表
POST ask
ChatRecord 对象
写入 chat.db
GET history
七、安全必修课:SQL 注入是怎么发生的
写数据库,绕不开一个安全概念------SQL 注入。它是最经典、危害最大的 Web 漏洞之一,理解它比会写 CRUD 更重要。
先说它是怎么发生的。 假设你写了个"按名字查用户"的接口,用字符串拼接 SQL:
python
# ❌ 危险写法:字符串拼接
name = input("输入用户名:") # 用户输入:' OR '1'='1
sql = f"SELECT * FROM users WHERE name = '{name}'"
如果用户输入的是 ' OR '1'='1,拼出来的 SQL 就变成:
sql
SELECT * FROM users WHERE name = '' OR '1'='1'
'1'='1' 永远为真,于是 WHERE 条件失效------这条 SQL 会把整张表的所有用户都查出来 。如果换成 ' OR '1'='1'; DROP TABLE users; --,甚至能删库。这就是注入:用户输入被当成 SQL 代码执行了,而不是被当成普通数据。
怎么防?------参数化查询(也叫预编译)。 关键就一条:把"SQL 结构"和"用户数据"分开,数据永远不参与 SQL 语法拼接。
python
# ✅ 安全写法:? 占位符 + 参数元组
name = input("输入用户名:")
cur.execute("SELECT * FROM users WHERE name = ?", (name,))
这里的 ? 是一个占位符 ,数据库先编译好 SQL 结构,再把 name 当作纯数据 填进去------即使 name 里含 ' OR '1'='1,它也只是一个字符串字面量,不会被当成 SQL 执行。SQLAlchemy 的 ORM 默认就是参数化查询,所以你用 ORM 写代码,天然免疫大部分注入。
记住这条铁律 :永远不要用字符串拼接(
+/ f-string)拼 SQL 和用户输入 。要么用?占位符,要么用 ORM。这是从入门到"靠谱"的分水岭。
八、常见坑与避坑指南
| 坑 | 原因 | 解决方案 |
|---|---|---|
| 增删改后数据没存进去 | SQLite 需要 commit() 才写盘 |
增删改后记得 session.commit() |
| 用字符串拼接 SQL | 触发 SQL 注入 | 用 ? 占位符或 ORM |
| 忘写主键 | 表无法唯一标识记录 | 每张表都设 id 主键 |
| 数据库连接不关 | 连接泄漏,越用越慢 | 用 Depends(get_db) + finally 关闭 |
| 换数据库要重写 SQL | 手写 SQL 语法有差异 | 用 ORM,只改连接 URL |
| 并发写同一行 | 两个请求同时改一条数据 | 用事务(transaction),后续进阶篇讲 |
总结:一张图带走
这课的核心,一句话串起来:
- 内存会失忆,数据库是笔记本 :数据写进磁盘(
.db文件)才能永久保存,这叫持久化。 - 关系型数据库 = Excel:表 / 行 / 列 / 主键 / 外键,用 SQL 做增删改查(CRUD)。
- SQLite 零安装 :Python 内置
sqlite3,学习阶段最省事,数据就是一个文件。 - ORM 是翻译官:用 Python 类操作数据库,SQLAlchemy 帮你翻译成 SQL,还能免疫大部分 SQL 注入。
- FastAPI + 数据库 = 有记忆的后端 :
Depends(get_db)管理连接,POST /ask存、GET /history查。 - 安全铁律 :永远不要拼接 SQL,用参数化查询(
?占位符)或 ORM。

三个立即可用的行动:
- 把第四、五、六节的代码亲手跑一遍(SQLite 零安装,直接能跑),感受"存进去 → 查出来";
- 给上一课的
/ask接口加上/history查询,真正拥有自己的"有记忆的 AI 后端"; - 故意用字符串拼接写一个查询,再输入
' OR '1'='1,亲眼看看 SQL 注入有多危险,再用参数化查询修掉它。
如果这篇对你有帮助,点个赞、收藏一下 ,下一篇我们讲部署上线------把这个带数据库的 AI 后端真正跑起来给别人用。
关注我,这个系列共 20 篇:Python 语法 → FastAPI 实战 → 数据库 → 部署上线 → AI 应用后端架构,一路带你到能独立写生产级后端。
你写数据库时踩过什么坑?是忘 commit() 导致数据"凭空消失",还是踩过 SQL 注入?评论区聊聊,我挨个回。
(本文代码基于 Python 3.10+ / SQLAlchemy 2.0 / sqlite3 内置模块撰写,有版本差异评论区提醒我。)