欢迎光临
我们一直在努力

Python 从入门到实战(十三):Flask + 数据库(让 Web 应用支持数据持久化与多人协作)

c6e3154d215982a75f2280a6ca6650b4.jpeg

文章目录

    • 一、为什么需要数据库?CSV 的 5 个致命不足
    • 二、选择工具:Flask-SQLAlchemy + SQLite
      • 1. 环境准备:安装依赖
      • 2. 核心概念:ORM 与数据模型
    • 三、实战 1:配置数据库与定义数据模型
      • 1. 项目结构升级
      • 2. 配置数据库:修改`app.py`
      • 3. 定义数据模型:创建`models.py`
        • 模型关键说明:
      • 4. 初始化数据库与导入 CSV 数据
        • 步骤 1:创建数据库表
        • 步骤 2:导入 CSV 数据到数据库
    • 四、实战 2:修改 Flask 路由(从数据库获取数据)
      • 1. 学生列表路由:从数据库查询所有学生与课程
      • 2. 成绩查询路由:从数据库筛选学生
      • 3. 可视化报告路由:从数据库统计数据
    • 五、实战 3:新增数据管理功能(增删改查)
      • 1. 新增学生功能(含课程)
        • 步骤 1:创建新增学生模板`templates/student_add.html`
        • 步骤 2:添加新增学生路由
      • 2. 编辑成绩功能
        • 步骤 1:创建编辑模板`templates/student_edit.html`
        • 步骤 2:添加编辑路由
      • 3. 删除学生功能
    • 六、新手必踩的 5 个坑:数据库操作避坑指南
      • 坑 1:忘记激活 Flask 应用上下文(无法操作数据库)
      • 坑 2:修改数据后忘记提交事务(数据不保存)
      • 坑 3:外键关联错误(课程无法关联学生)
      • 坑 4:重复添加数据(违反 unique 约束)
      • 坑 5:数据库模型变更后,表结构不更新
    • 七、小结与下一篇预告
      • 这篇你学到了什么?
      • 下一篇预告

欢迎回到「Python 从入门到实战」系列专栏。上一篇咱们用 Flask 开发了在线学生成绩管理系统,实现了成绩展示、查询和可视化,但有个致命问题:数据存储在
students_data.csv文件中。这意味着多人同时修改会导致数据冲突,新增学生、修改成绩后需要手动更新 CSV 文件,而且无法高效查询(比如查 “15 岁学生的英语平均分” 需要全量读取文件)。

今天咱们要引入数据库来彻底解决这些问题。选择轻量级的SQLite(无需安装,文件型数据库)和 Flask 生态的Flask-SQLAlchemy(ORM 工具,不用写复杂 SQL),把 CSV 数据迁移到数据库中,实现数据的高效查询、事务安全和多人协作。咱们会从 “数据库基础” 入手,逐步完成 “模型定义→数据迁移→功能升级”,让 Web 应用真正具备生产级别的数据管理能力。

一、为什么需要数据库?CSV 的 5 个致命不足

在学数据库操作前,先明确 “为什么要放弃 CSV 改用数据库”—— 对比两者的差异,才能理解数据库的价值:

对比维度CSV 文件数据库(SQLite)
多人协作 同时修改会覆盖数据,冲突严重 支持事务,多人操作安全无冲突
查询效率 全量读取文件,数据量大时卡顿 支持索引,毫秒级查询特定数据
数据完整性 无法限制数据格式(比如成绩输文字) 字段类型约束(成绩必须是整数)
数据管理 新增 / 修改 / 删除需手动改文件 支持增删改查(CRUD),操作便捷
功能扩展性 无法关联多表数据(比如学生 – 课程) 支持表关联,复杂数据关系轻松管理

简单说:CSV 适合小体量、单人使用的静态数据;数据库适合动态、多人协作、需要频繁修改的数据 —— 这正是 Web 应用的核心需求。

二、选择工具:Flask-SQLAlchemy + SQLite

为了降低新手门槛,咱们选择以下工具组合:

  • SQLite:轻量级文件型数据库,无需安装服务器,数据库就是一个.db文件,适合开发和小型应用;
  • Flask-SQLAlchemy:Flask 的 ORM(对象关系映射)扩展,能把 Python 类(比如Student类)映射为数据库表,不用写原生 SQL 语句,用 Python 代码就能操作数据库。

1. 环境准备:安装依赖

打开终端,安装 Flask-SQLAlchemy(以及上一篇的依赖,确保不遗漏):

bash

# 安装Flask-SQLAlchemy(ORM工具)
pip install flask-sqlalchemy
# 安装其他依赖(确保之前的功能正常)
pip install flask pandas matplotlib seaborn

2. 核心概念:ORM 与数据模型

ORM(Object-Relational Mapping)的核心是 “将 Python 对象与数据库表关联”:

  • 一个 Python 类 → 一个数据库表(比如Student类 → student表);
  • 类的一个属性 → 表的一个字段(比如Student.name → student表的name列);
  • 类的一个实例 → 表的一行数据(比如xiaoming = Student(name="小明") → student表的一行记录)。

这种方式让新手不用学习 SQL,用熟悉的 Python 语法就能操作数据库。

三、实战 1:配置数据库与定义数据模型

咱们先修改上一篇的 Flask 项目结构,新增数据库配置和数据模型,把之前的 “学生 – 课程” 数据映射为数据库表。

1. 项目结构升级

在原有结构基础上,新增models.py文件存放数据模型,新增instance文件夹存储 SQLite 数据库文件:

plaintext

student_web/
├── app.py # 主程序(路由、视图函数)
├── models.py # 数据模型(Student、Course类)
├── students_data.csv # 旧数据(用于导入数据库)
├── instance/ # 数据库文件存放目录(Flask默认)
│ └── student.db # SQLite数据库文件(自动生成)
├── templates/ # 模板文件(不变,新增表单模板)
│ ├── …(原有模板)
│ ├── student_add.html # 新增学生表单
│ └── student_edit.html # 编辑学生表单
└── static/ # 静态文件(不变)
└── images/

2. 配置数据库:修改app.py

在app.py中添加数据库配置,初始化 Flask-SQLAlchemy:

python

# app.py(新增数据库配置,放在文件开头)
from flask import Flask, render_template, request, redirect, url_for, flash
from flask_sqlalchemy import SQLAlchemy
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import os

# 1. 初始化Flask应用
app = Flask(__name__)

# 2. 配置SQLite数据库
# SQLALCHEMY_DATABASE_URI:数据库连接地址,SQLite的地址格式为"sqlite:///文件路径"
# instance/student.db:数据库文件存放在instance文件夹下
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///instance/student.db'
# 关闭SQLAlchemy的修改跟踪(减少资源占用,新手建议关闭)
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
# 配置秘钥(用于flash消息,提示用户操作结果)
app.config['SECRET_KEY'] = 'your-secret-key-here' # 随便填一段字符串,比如"student-system-2024"

# 3. 初始化SQLAlchemy对象(关联Flask应用)
db = SQLAlchemy(app)

# 4. 导入数据模型(必须在db初始化后导入)
from models import Student, Course

# 5. 中文字体配置(可视化用,不变)
plt.rcParams['font.sans-serif'] = ['SimHei', 'PingFang SC']
plt.rcParams['axes.unicode_minus'] = False
sns.set_style("whitegrid")
if not os.path.exists('static/images'):
os.makedirs('static/images')

3. 定义数据模型:创建models.py

根据之前的 CSV 数据,定义两个模型:Student(学生基本信息)和Course(学生的课程成绩),两者是 “一对多” 关系(一个学生可以有多个课程):

python

# models.py
from app import db # 从app导入初始化好的db对象
from datetime import datetime

# 1. 学生模型(对应student表)
class Student(db.Model):
# 定义表名(不指定则默认是类名小写,即"student")
__tablename__ = 'students'

# 主键(唯一标识一条记录,自增整数)
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
# 学生姓名(字符串,非空,唯一,避免重复)
name = db.Column(db.String(50), nullable=False, unique=True)
# 学生年龄(整数,非空,范围10-30)
age = db.Column(db.Integer, nullable=False)
# 创建时间(自动记录创建时间,不用手动赋值)
created_at = db.Column(db.DateTime, default=datetime.utcnow)

# 定义与Course的关联:一个Student对应多个Course(一对多)
# backref:在Course中可以通过student属性反向关联到Student
courses = db.relationship('Course', backref='student', lazy=True, cascade='all, delete-orphan')

# 定义__repr__方法,打印实例时更易读
def __repr__(self):
return f'<Student {self.name}>'

# 2. 课程成绩模型(对应course表)
class Course(db.Model):
__tablename__ = 'courses'

id = db.Column(db.Integer, primary_key=True, autoincrement=True)
# 课程名称(字符串,非空)
name = db.Column(db.String(50), nullable=False)
# 课程成绩(整数,非空,范围0-100)
score = db.Column(db.Integer, nullable=False)
# 外键:关联到students表的id字段(一个学生对应多个课程)
student_id = db.Column(db.Integer, db.ForeignKey('students.id'), nullable=False)

def __repr__(self):
return f'<Course {self.name}{self.score}>'

模型关键说明:
  • 字段类型:db.String(50)(字符串,最大长度 50)、db.Integer(整数)、db.DateTime(时间);
  • 约束:nullable=False(字段不能为空)、unique=True(字段值唯一,避免重复学生);
  • 关联关系:Student.courses通过relationship关联Course,Course.student_id通过ForeignKey关联Student.id,实现 “学生 – 课程” 的一对多关系;
  • 级联删除:cascade='all, delete-orphan'表示删除学生时,自动删除该学生的所有课程,避免数据残留。

4. 初始化数据库与导入 CSV 数据

定义好模型后,需要创建数据库表,并把之前 CSV 的数据导入到数据库中,实现 “CSV→数据库” 的迁移。

步骤 1:创建数据库表

在项目根目录下,打开 Python 终端,执行以下命令(手动初始化数据库):

python

# 进入Python终端
python

# 在Python终端中执行:
from app import app, db, Student, Course
from models import Student, Course

# 激活Flask应用上下文(必须,否则无法操作数据库)
with app.app_context():
# 创建所有模型对应的表(如果表不存在)
db.create_all()
print("数据库表创建成功!")

执行后,会在instance文件夹下生成student.db文件,这就是 SQLite 数据库。

步骤 2:导入 CSV 数据到数据库

在app.py中添加一个临时路由,用于导入 CSV 数据(执行一次后可删除):

python

# app.py(临时路由,导入CSV数据,执行一次后注释)
@app.route('/import/csv')
def import_csv():
with app.app_context():
# 1. 读取CSV数据
df = pd.read_csv('students_data.csv', encoding='utf-8', dtype={'course_score': int})

# 2. 遍历CSV行,导入数据库
for _, row in df.iterrows():
# 检查学生是否已存在(避免重复导入)
student = Student.query.filter_by(name=row['name']).first()
if not student:
# 新增学生
student = Student(name=row['name'], age=row['age'])
db.session.add(student)
db.session.commit() # 提交事务,获取学生id

# 新增课程(关联到学生)
course = Course(
name=row['course_name'],
score=row['course_score'],
student_id=student.id # 关联学生的id
)
db.session.add(course)

# 提交所有数据
db.session.commit()
flash('CSV数据导入数据库成功!', 'success')
return redirect(url_for('index')) # 跳转到首页

执行方式:运行app.py,访问http://localhost:5000/import/csv,看到 “数据导入成功” 的提示后,即可注释或删除该路由(避免重复导入)。

四、实战 2:修改 Flask 路由(从数据库获取数据)

之前的路由都是从 CSV 读取数据,现在要全部改成从数据库查询。咱们以 “学生列表”“成绩查询”“可视化报告” 三个核心功能为例,展示如何用 SQLAlchemy 操作数据库。

1. 学生列表路由:从数据库查询所有学生与课程

python

# app.py(修改学生列表路由,替换CSV逻辑)
@app.route('/student/list')
def student_list():
with app.app_context():
# 1. 查询所有学生(关联查询课程,用join避免N+1查询问题)
students = Student.query.all()

# 2. 整理数据(学生+课程)
student_data = []
for student in students:
# 遍历学生的所有课程(通过student.courses关联)
for course in student.courses:
# 计算成绩等级
if course.score >= 90:
grade = 'A级(90+)'
elif course.score >= 80:
grade = 'B级(80-89)'
else:
grade = 'C级(<80)'
student_data.append({
'name': student.name,
'age': student.age,
'course_name': course.name,
'course_score': course.score,
'grade': grade
})

# 3. 统计信息
total_students = Student.query.count() # 总学生数(SQLAlchemy的count()方法)
total_courses = Course.query.count() # 总课程数

return render_template(
'student_list.html',
students=student_data,
total_students=total_students,
total_courses=total_courses
)

2. 成绩查询路由:从数据库筛选学生

python

# app.py(修改成绩查询路由,替换CSV逻辑)
@app.route('/student/search', methods=['GET', 'POST'])
def student_search():
if request.method == 'GET':
return render_template('student_search.html')

elif request.method == 'POST':
student_name = request.form.get('student_name', '').strip()
if not student_name:
flash('请输入学生姓名!', 'danger')
return render_template('student_search.html')

with app.app_context():
# 1. 查询学生(关联查询课程)
student = Student.query.filter_by(name=student_name).first()
if not student:
flash(f'未找到名为“{student_name}”的学生', 'danger')
return render_template('student_search.html', input_name=student_name)

# 2. 计算总分和平均分
courses = student.courses
total_score = sum(course.score for course in courses)
avg_score = total_score / len(courses) if courses else 0

# 3. 整理课程数据
student_courses = []
for course in courses:
if course.score >= 90:
grade = 'A级(90+)'
elif course.score >= 80:
grade = 'B级(80-89)'
else:
grade = 'C级(<80)'
student_courses.append({
'course_name': course.name,
'course_score': course.score,
'grade': grade
})

return render_template(
'student_search.html',
input_name=student_name,
student=student_courses,
total_score=total_score,
avg_score=round(avg_score, 1)
)

3. 可视化报告路由:从数据库统计数据

python

# app.py(修改可视化报告路由,替换CSV逻辑)
@app.route('/report')
def report():
with app.app_context():
# 1. 查询所有课程成绩,统计各科平均分
# SQLAlchemy分组统计:按课程名分组,计算成绩平均值
course_avg_query = db.session.query(
Course.name, db.func.avg(Course.score).label('avg_score')
).group_by(Course.name).all()
# 转换为字典,方便绘图
course_avg = {row.name: round(row.avg_score, 1) for row in course_avg_query}

# 2. 统计成绩等级分布
# 查询所有成绩,分类计数
all_scores = [course.score for course in Course.query.all()]
grade_count = {
'A级(90+)': sum(1 for s in all_scores if s >= 90),
'B级(80-89)': sum(1 for s in all_scores if 80 <= s < 90),
'C级(<80)': sum(1 for s in all_scores if s < 80)
}

# 3. 生成柱状图(各科平均分)
fig, ax = plt.subplots(figsize=(6, 4), dpi=100)
bars = ax.bar(course_avg.keys(), course_avg.values(), color='skyblue', edgecolor='black')
ax.set_title('各科平均分对比', fontsize=12)
ax.set_xlabel('课程名称')
ax.set_ylabel('平均分(分)')
ax.set_ylim(80, 90)
for bar in bars:
height = bar.get_height()
ax.text(bar.get_x()+bar.get_width()/2, height+0.5, str(height), ha='center', va='bottom')
plt.savefig('static/images/course_avg.png', bbox_inches='tight')
plt.close()

# 4. 生成饼图(成绩等级分布)
fig, ax = plt.subplots(figsize=(6, 6), dpi=100)
colors = ['#FF6B6B', '#4ECDC4', '#45B7D1']
wedges, texts, autotexts = ax.pie(
grade_count.values(), labels=grade_count.keys(), autopct='%1.1f%%',
startangle=90, colors=colors, explode=(0.05, 0, 0)
)
ax.set_title('成绩等级分布', fontsize=12)
for autotext in autotexts:
autotext.set_color('white')
plt.savefig('static/images/grade_pie.png', bbox_inches='tight')
plt.close()

return render_template('report.html')

五、实战 3:新增数据管理功能(增删改查)

数据库的核心优势是支持动态修改数据,咱们新增三个实用功能:新增学生、编辑成绩、删除学生,这些是 CSV 无法实现的。

1. 新增学生功能(含课程)

步骤 1:创建新增学生模板templates/student_add.html

html

<!– templates/student_add.html –>
{% extends "base.html" %}

{% block title %}新增学生 – 学生成绩管理系统{% endblock %}

{% block content %}
<div class="row justify-content-center">
<div class="col-md-8">
<h2>新增学生与课程成绩</h2>
<form method="POST" class="mt-4">
<!– 学生基本信息 –>
<div class="card mb-3">
<div class="card-header">学生基本信息</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">学生姓名</label>
<input type="text" name="name" class="form-control" required>
<div class="form-text">姓名唯一,不可重复</div>
</div>
<div class="col-md-6">
<label class="form-label">学生年龄</label>
<input type="number" name="age" class="form-control" min="10" max="30" required>
</div>
</div>
</div>
</div>

<!– 课程成绩(支持新增2门课程,可扩展) –>
<div class="card mb-3">
<div class="card-header">课程成绩(至少填写1门)</div>
<div class="card-body">
<!– 课程1 –>
<div class="row g-3 mb-3">
<div class="col-md-6">
<label class="form-label">课程名称1</label>
<input type="text" name="course1_name" class="form-control" required>
</div>
<div class="col-md-6">
<label class="form-label">成绩1</label>
<input type="number" name="course1_score" class="form-control" min="0" max="100" required>
</div>
</div>
<!– 课程2 –>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">课程名称2(可选)</label>
<input type="text" name="course2_name" class="form-control">
</div>
<div class="col-md-6">
<label class="form-label">成绩2(可选)</label>
<input type="number" name="course2_score" class="form-control" min="0" max="100">
</div>
</div>
</div>
</div>

<button type="submit" class="btn btn-primary">提交新增</button>
<a href="{{ url_for('student_list') }}" class="btn btn-secondary ms-2">取消</a>
</form>
</div>
</div>
{% endblock %}

步骤 2:添加新增学生路由

python

# app.py(新增学生路由)
@app.route('/student/add', methods=['GET', 'POST'])
def student_add():
if request.method == 'GET':
return render_template('student_add.html')

elif request.method == 'POST':
# 1. 获取表单数据
name = request.form.get('name', '').strip()
age = request.form.get('age', '')
course1_name = request.form.get('course1_name', '').strip()
course1_score = request.form.get('course1_score', '')
course2_name = request.form.get('course2_name', '').strip()
course2_score = request.form.get('course2_score', '')

# 2. 验证数据
errors = []
if not name:
errors.append('学生姓名不能为空')
if not age.isdigit() or not (10 <= int(age) <= 30):
errors.append('年龄必须是10-30之间的整数')
if not course1_name or not course1_score.isdigit() or not (0 <= int(course1_score) <= 100):
errors.append('第一门课程名称和成绩不能为空,成绩必须是0-100之间的整数')

if errors:
for err in errors:
flash(err, 'danger')
return render_template('student_add.html')

# 转换数据类型
age = int(age)
course1_score = int(course1_score)

with app.app_context():
# 3. 检查学生是否已存在
if Student.query.filter_by(name=name).first():
flash(f'学生“{name}”已存在,不可重复添加', 'danger')
return render_template('student_add.html')

# 4. 新增学生
new_student = Student(name=name, age=age)
db.session.add(new_student)
db.session.commit() # 提交获取学生id

# 5. 新增第一门课程
new_course1 = Course(
name=course1_name,
score=course1_score,
student_id=new_student.id
)
db.session.add(new_course1)

# 6. 新增第二门课程(如果有数据)
if course2_name and course2_score.isdigit():
course2_score = int(course2_score)
if 0 <= course2_score <= 100:
new_course2 = Course(
name=course2_name,
score=course2_score,
student_id=new_student.id
)
db.session.add(new_course2)

# 7. 提交所有数据
db.session.commit()
flash(f'学生“{name}”及课程新增成功!', 'success')
return redirect(url_for('student_list')) # 跳转到学生列表

2. 编辑成绩功能

步骤 1:创建编辑模板templates/student_edit.html

html

<!– templates/student_edit.html –>
{% extends "base.html" %}

{% block title %}编辑成绩 – 学生成绩管理系统{% endblock %}

{% block content %}
<div class="row justify-content-center">
<div class="col-md-8">
<h2>编辑 {{ student.name }} 的成绩</h2>
<form method="POST" class="mt-4">
<!– 学生姓名不可修改(只读) –>
<div class="mb-3">
<label class="form-label">学生姓名</label>
<input type="text" class="form-control" value="{{ student.name }}" readonly>
<input type="hidden" name="student_id" value="{{ student.id }}"> <!– 隐藏字段,传递学生id –>
</div>

<!– 课程成绩编辑 –>
<div class="card mb-3">
<div class="card-header">课程成绩</div>
<div class="card-body">
{% for course in student.courses %}
<div class="row g-3 mb-3">
<div class="col-md-6">
<label class="form-label">课程名称</label>
<input type="text" name="course_name_{{ course.id }}" class="form-control" value="{{ course.name }}" required>
</div>
<div class="col-md-6">
<label class="form-label">成绩</label>
<input type="number" name="course_score_{{ course.id }}" class="form-control" min="0" max="100" value="{{ course.score }}" required>
</div>
</div>
{% endfor %}
</div>
</div>

<button type="submit" class="btn btn-primary">保存修改</button>
<a href="{{ url_for('student_search', student_name=student.name) }}" class="btn btn-secondary ms-2">取消</a>
</form>
</div>
</div>
{% endblock %}

步骤 2:添加编辑路由

python

# app.py(编辑成绩路由)
@app.route('/student/edit/<int:student_id>', methods=['GET', 'POST'])
def student_edit(student_id):
with app.app_context():
# 查询学生(关联课程)
student = Student.query.get(student_id)
if not student:
flash('未找到该学生', 'danger')
return redirect(url_for('student_list'))

if request.method == 'GET':
# GET请求:显示编辑表单,传递学生数据
return render_template('student_edit.html', student=student)

elif request.method == 'POST':
# 1. 获取学生id(隐藏字段)
student_id = request.form.get('student_id')
if not student_id or not student_id.isdigit():
flash('学生信息错误', 'danger')
return render_template('student_edit.html', student=student)

# 2. 遍历学生的所有课程,更新成绩
for course in student.courses:
# 获取该课程的新名称和成绩(表单字段名:course_name_课程id)
new_name = request.form.get(f'course_name_{course.id}', '').strip()
new_score = request.form.get(f'course_score_{course.id}', '')

# 验证数据
if not new_name:
flash(f'课程名称不能为空', 'danger')
return render_template('student_edit.html', student=student)
if not new_score.isdigit() or not (0 <= int(new_score) <= 100):
flash(f'课程“{new_name}”的成绩必须是0-100之间的整数', 'danger')
return render_template('student_edit.html', student=student)

# 更新课程数据
course.name = new_name
course.score = int(new_score)

# 3. 提交修改
db.session.commit()
flash(f'学生“{student.name}”的成绩修改成功!', 'success')
return redirect(url_for('student_search', student_name=student.name))

3. 删除学生功能

在学生列表或查询结果中添加删除按钮,点击后删除学生及其所有课程:

python

# app.py(删除学生路由)
@app.route('/student/delete/<int:student_id>', methods=['POST'])
def student_delete(student_id):
with app.app_context():
# 查询学生
student = Student.query.get(student_id)
if not student:
flash('未找到该学生', 'danger')
return redirect(url_for('student_list'))

# 删除学生(级联删除课程,因为模型中设置了cascade='all, delete-orphan')
db.session.delete(student)
db.session.commit()
flash(f'学生“{student.name}”及所有课程已删除', 'success')
return redirect(url_for('student_list'))

在学生列表模板中添加删除按钮(student_list.html):

html

<!– 在student_list.html的表格中新增一列 –>
<thead class="table-dark">
<tr>
<th>学生姓名</th>
<th>年龄</th>
<th>课程名称</th>
<th>成绩(分)</th>
<th>成绩等级</th>
<th>操作</th> <!– 新增操作列 –>
</tr>
</thead>
<tbody>
{% for student in students %}
<tr>
<td>{{ student.name }}</td>
<td>{{ student.age }}</td>
<td>{{ student.course_name }}</td>
<td>{{ student.course_score }}</td>
<td></td> <!– 成绩等级 –>
<td>
<!– 编辑按钮:跳转到编辑页面 –>
<a href="{{ url_for('student_edit', student_id=student.id) }}" class="btn btn-sm btn-warning">编辑</a>
<!– 删除按钮:POST请求,避免误点击 –>
<form method="POST" action="{{ url_for('student_delete', student_id=student.id) }}" class="d-inline">
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('确定要删除吗?')">删除</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>

六、新手必踩的 5 个坑:数据库操作避坑指南

数据库操作对新手来说容易踩坑,尤其是 ORM 和事务相关的问题,总结如下:

坑 1:忘记激活 Flask 应用上下文(无法操作数据库)

python

# 错误示例:直接操作数据库,未激活上下文
from app import db, Student
student = Student.query.first() # 报错:RuntimeError: Working outside of application context.

解决:必须在with app.app_context():块中操作数据库:

python

with app.app_context():
student = Student.query.first() # 正确

坑 2:修改数据后忘记提交事务(数据不保存)

python

# 错误示例:更新成绩后未commit()
with app.app_context():
course = Course.query.get(1)
course.score = 90 # 修改数据
# 忘记db.session.commit(),数据不会保存到数据库

解决:所有增删改操作后,必须调用db.session.commit():

python

with app.app_context():
course = Course.query.get(1)
course.score = 90
db.session.commit() # 提交事务,数据才会保存

坑 3:外键关联错误(课程无法关联学生)

python

# 错误示例:新增课程时student_id不存在
with app.app_context():
new_course = Course(name="数学", score=85, student_id=999) # student_id=999不存在
db.session.add(new_course)
db.session.commit() # 报错:IntegrityError: FOREIGN KEY constraint failed

解决:确保student_id对应的学生存在,或通过关联属性赋值:

python

with app.app_context():
student = Student.query.get(1) # 先查询存在的学生
new_course = Course(name="数学", score=85, student=student) # 直接关联学生实例
db.session.add(new_course)
db.session.commit() # 正确

坑 4:重复添加数据(违反 unique 约束)

python

# 错误示例:新增重复姓名的学生(name设置了unique=True)
with app.app_context():
new_student = Student(name="小明", age=15) # 小明已存在
db.session.add(new_student)
db.session.commit() # 报错:IntegrityError: UNIQUE constraint failed

解决:新增前先查询,避免重复:

python

with app.app_context():
if not Student.query.filter_by(name="小明").first():
new_student = Student(name="小明", age=15)
db.session.add(new_student)
db.session.commit()
else:
print("学生已存在")

坑 5:数据库模型变更后,表结构不更新

python

# 错误示例:修改Student模型(新增gender字段),表结构不变
class Student(db.Model):
# 新增gender字段
gender = db.Column(db.String(10), default="男")

解决:使用Flask-Migrate处理模型变更(简单步骤):

bash

# 安装Flask-Migrate
pip install flask-migrate

在app.py中配置:

python

from flask_migrate import Migrate
migrate = Migrate(app, db)

然后执行迁移命令:

bash

# 初始化迁移环境(第一次执行)
flask db init
# 生成迁移脚本
flask db migrate -m "add gender to student"
# 应用迁移(更新表结构)
flask db upgrade

七、小结与下一篇预告

这篇你学到了什么?

  • 数据库的价值:理解 CSV 的不足,数据库在多人协作、数据安全、高效查询上的优势;
  • 工具选择:用 SQLite(轻量级)和 Flask-SQLAlchemy(ORM),新手无需写原生 SQL;
  • 核心操作:配置数据库、定义数据模型(Student、Course)、初始化表、导入 CSV 数据;
  • 功能升级:将 Flask 路由从 CSV 迁移到数据库,新增 “新增学生”“编辑成绩”“删除学生” 功能;
  • 避坑指南:解决应用上下文、事务提交、外键关联等常见问题。
  • 下一篇预告

    今天的系统已经具备完整的数据管理能力,但界面还比较简单,且没有用户权限控制(任何人都能删除学生)。下一篇咱们会学习Flask 用户认证(用 Flask-Login),实现 “管理员登录” 功能,区分管理员和普通用户权限(比如普通用户只能查询,管理员能增删改),同时美化界面,让系统更安全、更专业。

    如果这篇内容帮你成功将 CSV 迁移到数据库,欢迎在评论区分享你的操作心得或遇到的问题,咱们一起交流进步~

    赞(0)
    未经允许不得转载:171主机测评 » Python 从入门到实战(十三):Flask + 数据库(让 Web 应用支持数据持久化与多人协作)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址