前端页面设计做久了,慢慢地对页面字段与数据库数据之间的关系有了更清晰的认识,于是我开始尝试打通前后端、与数据库之间的连接。
我在实践之后,把"最小可行版本"整理成这篇文章,方便新手朋友们一起学习:用最少的文件,让页面按钮真正把数据写进 MySQL。
一、明确一个关键认知:先有数据库表,再有前端页面
前端页面不是凭空设计出来的,它本质上是数据库 Schema 的"可视化操作面板"。
-
数据库字段(类型、是否可空、主键/外键)决定了前端表单字段、校验规则、下拉选项、列表列名
-
数据库表之间的关系决定了页面联动与联表展示(比如 employee.branch_id → branch.branch_id)
所以正确顺序通常是:
建库建表(Schema) → DB 层(连接/执行) → API 层(路由/参数解析) → 前端页面(fetch 调用接口)
1. 1 最小可行架构:3 个文件 + 2 个目录
本篇文章用最小结构跑通 查询/新增/修改/删除(CRUD)。
注意:HTML 必须放在
templates/下,因为 Flask 的模板渲染有"默认规则"。
sql连接/
coffee_app.py # Flask 入口:路由 /api/...
coffee_db.py # DB 层:连接 + query/execute + commit
templates/
coffee_admin.html # 前端页面:按钮 fetch 调接口
1.2 常见问题:
在联调时很容易因为疏忽导致:
前端页面中看似插入了新数据记录,但实际上只是页面里 mock 数据,并没有真实写入数据库。
快速判断方法:
打开浏览器 F12 → Network:
-
如果只看到:
GET /api/employees 200没看到:
POST /api/employees 201
大概率:保存按钮没有绑定 POST 请求(只是改了前端页面展示) -
如果看到:
POST /api/employees 201但 Workbench 里查不到新增数据
大概率:后端写入缺少 commit(201 不等于落库)
这些问题本质都在提醒:只有当表结构、接口契约、前端交互三者对齐,CRUD 才算真正跑通。
1.3 准备数据库:
我这里以一个常见场景------商场员工及顾客消费信息作为数据源 ,建立 MySQL 的 coffee 数据库(你也可以换成自己的业务)。
这里的SQL语句分别创建了branch(员工所属分支表) 、client(客户信息表)、employee(员工表)、work_with (连接员工和客户,并记员工的销售业绩。)四个表。
sql
CREATE TABLE `branch` (
`branch_id` int NOT NULL,
`branch_name` varchar(20) DEFAULT NULL,
`manager_id` int DEFAULT NULL,
PRIMARY KEY (`branch_id`),
KEY `manager_id` (`manager_id`),
CONSTRAINT `branch_ibfk_1` FOREIGN KEY (`manager_id`) REFERENCES `employee` (`emp_id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
sql
CREATE TABLE `client` (
`client_id` int NOT NULL,
`client_name` varchar(20) DEFAULT NULL,
`phone_num` varchar(32) DEFAULT NULL,
PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
sql
CREATE TABLE `employee` (
`emp_id` int NOT NULL,
`name` varchar(20) DEFAULT NULL,
`birth_date` date DEFAULT NULL,
`sex` varchar(1) DEFAULT NULL,
`salary` int DEFAULT NULL,
`branch_id` int DEFAULT NULL,
`sup_id` int DEFAULT NULL,
PRIMARY KEY (`emp_id`),
KEY `branch_id` (`branch_id`),
KEY `sup_id` (`sup_id`),
CONSTRAINT `employee_ibfk_1` FOREIGN KEY (`branch_id`) REFERENCES `branch` (`branch_id`) ON DELETE SET NULL,
CONSTRAINT `employee_ibfk_2` FOREIGN KEY (`sup_id`) REFERENCES `employee` (`emp_id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
sql
CREATE TABLE `work_with` (
`emp_id` int NOT NULL,
`client_id` int NOT NULL,
`total_sales` int DEFAULT NULL,
PRIMARY KEY (`emp_id`,`client_id`),
KEY `client_id` (`client_id`),
CONSTRAINT `work_with_ibfk_1` FOREIGN KEY (`emp_id`) REFERENCES `employee` (`emp_id`) ON DELETE CASCADE,
CONSTRAINT `work_with_ibfk_2` FOREIGN KEY (`client_id`) REFERENCES `client` (`client_id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
在 Python 准备连接数据库之前,建议先在 Workbench 执行三条自检:
sql
SELECT DATABASE();
SHOW TABLES;
SELECT COUNT(*) FROM employee;
来确认当前连接的是哪个库、表是否存在,表里是否有数据。
二、前后端联动
2.1 文件 1:coffee_db.py(数据库连接与执行层)
这一层的职责是:
-
提供连接数据库的条件(host/port/user/password/database/charset)
-
封装执行 SQL 的方法
-
写操作必须 commit;出错 rollback
注意:查询不需要 commit,只有 insert/update/delete(写)才需要。
1、 获取连接
python
# coffee_db.py
import pymysql
def get_conn():
return pymysql.Connection(
host="127.0.0.1",
port=3306,
user="root",
password="123456",
database="coffee",
charset="utf8mb4",
cursorclass=pymysql.cursors.DictCursor,
autocommit=False, # 推荐:手动 commit 更可控
)
2、 query:查(SELECT)→ 返回 rows
python
# coffee_db.py
def query(sql, params=None):
conn = get_conn()
try:
with conn.cursor() as cur:
cur.execute(sql, params or ())
return cur.fetchall() # rows
finally:
conn.close()
3、 execute:写(INSERT/UPDATE/DELETE)→ 返回 affected_rows(并 commit)
python
# coffee_db.py
def execute(sql, params=None):
conn = get_conn()
try:
with conn.cursor() as cur:
affected = cur.execute(sql, params or ()) # affected_rows
conn.commit() # 写操作必须提交
return affected
except:
conn.rollback() # 出错撤销本次操作
raise
finally:
conn.close()
你可以这样理解 DB 返回的两类结果:
-
rows:SELECT 查出来的多行数据(给前端表格渲染用) -
affected_rows:写操作影响了多少行(判断是否真的生效)
2.2. 文件 2:coffee_admin.html(前端页面:按钮绑定 fetch)
最初我以为"写了表单 + 写了按钮 = 能写数据库",其实不对。
真正能写进数据库的关键是:按钮点击时必须发出 POST/PUT/DELETE 请求。
下面是示例:
1、 GET:加载 employees 列表
html
<!-- templates/coffee_admin.html -->
<button id="btnRefresh">刷新 Employees</button>
<script>
async function loadEmployees() {
const res = await fetch("/api/employees"); // GET
const data = await res.json();
console.log("employees:", data.data);
}
document.getElementById("btnRefresh").addEventListener("click", loadEmployees);
loadEmployees();
</script>
2、 POST:保存按钮绑定新增员工(核心)
html
<button id="btnSave">保存(新增员工)</button>
<script>
document.getElementById("btnSave").addEventListener("click", async () => {
const payload = {
emp_id: 211,
name: "小红",
birth_date: "1999-02-01",
sex: "F",
salary: 30000,
branch_id: 1,
sup_id: 206
};
const res = await fetch("/api/employees", {
method: "POST",
headers: {"Content-Type":"application/json"},
body: JSON.stringify(payload)
});
const data = await res.json();
console.log("create result:", res.status, data);
// 成功后刷新列表:关闭弹窗/提示 toast/重新 GET
if (res.ok && data.ok) {
await loadEmployees();
}
});
</script>
你会看到这里的固定规律:
-
GET 查询:通常参数放在 URL query(
?q=...),不需要 body -
POST/PUT 写入:需要 JSON body,所以需要head中说明body中结构(比如head中是
Content-Type: application/json,body中是:JSON.stringify(payload) -
DELETE 删除:通常靠 URL 传 id
2.3 文件 3:coffee_app.py(Flask API:CRUD 真正发生)
先明确一个认识:
增删查改真正发生在后端(API 接口)里。
前端按钮本身不会直接操作 MySQL,它只是发 HTTP 请求(GET/POST/PUT/DELETE)。
Flask 的 API 文件通过路由"截获请求",解析 request,执行 SQL(或调用 db 层执行),再返回 JSON 响应。
可将整体交互链路理解为:
-
前端按钮 = 触发请求(fetch发出请求)
-
API 路由 = 接收请求 + 执行业务(SQL)
-
数据库 = 数据落点
1、 先让 Flask 能返回页面(否则你只会看到默认页)
python
# coffee_app.py
from flask import Flask, request, jsonify, render_template
import coffee_db as db
app = Flask(__name__)
@app.get("/") #截取路由地址,截取到"/"才执行后续代码。
def home():
return render_template("coffee_admin.html")
2、 GET:支持 Query 参数筛选(q / branch_id)
python
@app.get("/api/employees")
def list_employees():
q = (request.args.get("q") or "").strip()
branch_id = (request.args.get("branch_id") or "").strip()
sql = """
SELECT e.emp_id, e.name, e.birth_date, e.sex, e.salary, e.branch_id,
b.branch_name, e.sup_id, s.name AS sup_name
FROM employee e
LEFT JOIN branch b ON e.branch_id=b.branch_id
LEFT JOIN employee s ON e.sup_id=s.emp_id
WHERE 1=1
"""
params = []
if q:
sql += " AND (CAST(e.emp_id AS CHAR) LIKE %s OR e.name LIKE %s)"
params += [f"%{q}%", f"%{q}%"]
if branch_id:
sql += " AND e.branch_id=%s"
params += [branch_id]
rows = db.query(sql, params)
return jsonify({"ok": True, "data": rows})
对应前端调用:
html
fetch("/api/employees?q=小黄&branch_id=1")
3、 POST:新增员工(request.get_json → INSERT → commit)
@app.post("/api/employees")
def create_employee():
body = request.get_json(force=True)
sql = """
INSERT INTO employee(emp_id, name, birth_date, sex, salary, branch_id, sup_id)
VALUES (%s,%s,%s,%s,%s,%s,%s)
"""
affected = db.execute(sql, (
body["emp_id"], body["name"], body["birth_date"], body["sex"],
body["salary"], body["branch_id"], body.get("sup_id")
))
return jsonify({"ok": True, "data": {"affected": affected}}), 201
4、 启动服务
python
if __name__ == "__main__": #运行脚本,才能将服务器启动。
app.run(host="127.0.0.1", port=5000, debug=True)
三、关系图示
四、 小结
最小可行打通其实就三件事:
-
DB 层:能连库、能执行 SQL、写操作 commit
-
API 层:路由匹配 method+path,解析 request,把 query/body 映射到 SQL
-
前端:按钮真的发出 POST/PUT/DELETE(而不是只改页面)
只要这三层对齐,你就能从"页面 demo"升级到"真实 CRUD 系统"。
光说不练是难以看到成效的,所以可以多多尝试,结合以上示例动手试试。欢迎你有任何的问题,在评论中与我讨论 ~
