
文章目录
-
- [一、为什么需要数据库?CSV 的 5 个致命不足](#一、为什么需要数据库?CSV 的 5 个致命不足)
- [二、选择工具:Flask-SQLAlchemy + SQLite](#二、选择工具:Flask-SQLAlchemy + SQLite)
-
- [1. 环境准备:安装依赖](#1. 环境准备:安装依赖)
- [2. 核心概念:ORM 与数据模型](#2. 核心概念:ORM 与数据模型)
- [三、实战 1:配置数据库与定义数据模型](#三、实战 1:配置数据库与定义数据模型)
-
- [1. 项目结构升级](#1. 项目结构升级)
- [2. 配置数据库:修改`app.py`](#2. 配置数据库:修改
app.py) - [3. 定义数据模型:创建`models.py`](#3. 定义数据模型:创建
models.py) - [4. 初始化数据库与导入 CSV 数据](#4. 初始化数据库与导入 CSV 数据)
-
- [步骤 1:创建数据库表](#步骤 1:创建数据库表)
- [步骤 2:导入 CSV 数据到数据库](#步骤 2:导入 CSV 数据到数据库)
- [四、实战 2:修改 Flask 路由(从数据库获取数据)](#四、实战 2:修改 Flask 路由(从数据库获取数据))
-
- [1. 学生列表路由:从数据库查询所有学生与课程](#1. 学生列表路由:从数据库查询所有学生与课程)
- [2. 成绩查询路由:从数据库筛选学生](#2. 成绩查询路由:从数据库筛选学生)
- [3. 可视化报告路由:从数据库统计数据](#3. 可视化报告路由:从数据库统计数据)
- [五、实战 3:新增数据管理功能(增删改查)](#五、实战 3:新增数据管理功能(增删改查))
-
- [1. 新增学生功能(含课程)](#1. 新增学生功能(含课程))
-
- [步骤 1:创建新增学生模板`templates/student_add.html`](#步骤 1:创建新增学生模板
templates/student_add.html) - [步骤 2:添加新增学生路由](#步骤 2:添加新增学生路由)
- [步骤 1:创建新增学生模板`templates/student_add.html`](#步骤 1:创建新增学生模板
- [2. 编辑成绩功能](#2. 编辑成绩功能)
-
- [步骤 1:创建编辑模板`templates/student_edit.html`](#步骤 1:创建编辑模板
templates/student_edit.html) - [步骤 2:添加编辑路由](#步骤 2:添加编辑路由)
- [步骤 1:创建编辑模板`templates/student_edit.html`](#步骤 1:创建编辑模板
- [3. 删除学生功能](#3. 删除学生功能)
- [六、新手必踩的 5 个坑:数据库操作避坑指南](#六、新手必踩的 5 个坑:数据库操作避坑指南)
-
- [坑 1:忘记激活 Flask 应用上下文(无法操作数据库)](#坑 1:忘记激活 Flask 应用上下文(无法操作数据库))
- [坑 2:修改数据后忘记提交事务(数据不保存)](#坑 2:修改数据后忘记提交事务(数据不保存))
- [坑 3:外键关联错误(课程无法关联学生)](#坑 3:外键关联错误(课程无法关联学生))
- [坑 4:重复添加数据(违反 unique 约束)](#坑 4:重复添加数据(违反 unique 约束))
- [坑 5:数据库模型变更后,表结构不更新](#坑 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
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
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
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
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
# 在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
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
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
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
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
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
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
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
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
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
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
python
# 错误示例:直接操作数据库,未激活上下文
from app import db, Student
student = Student.query.first() # 报错:RuntimeError: Working outside of application context.
解决 :必须在with app.app_context():块中操作数据库:
python
python
with app.app_context():
student = Student.query.first() # 正确
坑 2:修改数据后忘记提交事务(数据不保存)
python
python
# 错误示例:更新成绩后未commit()
with app.app_context():
course = Course.query.get(1)
course.score = 90 # 修改数据
# 忘记db.session.commit(),数据不会保存到数据库
解决 :所有增删改操作后,必须调用db.session.commit():
python
python
with app.app_context():
course = Course.query.get(1)
course.score = 90
db.session.commit() # 提交事务,数据才会保存
坑 3:外键关联错误(课程无法关联学生)
python
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
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
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
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
python
# 错误示例:修改Student模型(新增gender字段),表结构不变
class Student(db.Model):
# 新增gender字段
gender = db.Column(db.String(10), default="男")
解决 :使用Flask-Migrate处理模型变更(简单步骤):
bash
bash
# 安装Flask-Migrate
pip install flask-migrate
在app.py中配置:
python
python
from flask_migrate import Migrate
migrate = Migrate(app, db)
然后执行迁移命令:
bash
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 迁移到数据库,欢迎在评论区分享你的操作心得或遇到的问题,咱们一起交流进步~