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

文章目录

    • [一、为什么需要数据库?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:添加新增学生路由)
      • [2. 编辑成绩功能](#2. 编辑成绩功能)
        • [步骤 1:创建编辑模板`templates/student_edit.html`](#步骤 1:创建编辑模板templates/student_edit.html)
        • [步骤 2:添加编辑路由](#步骤 2:添加编辑路由)
      • [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.namestudent表的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关联CourseCourse.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

七、小结与下一篇预告

这篇你学到了什么?

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

下一篇预告

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

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

相关推荐
BoBoZz192 小时前
Vol 建一个 3D 隐式函数体积数据
python·vtk·图形渲染·图形处理
jiayong232 小时前
AI应用领域编程语言选择指南:Java vs Python vs Go
java·人工智能·python
_illusion_2 小时前
反向传播的人生哲学:深度复盘的力量
人工智能·python·机器学习
博大世界2 小时前
Python打包成exe文件方法
开发语言·python
算法与编程之美2 小时前
解决tensor的shape不为1,如何转移到CPU的问题
人工智能·python·深度学习·算法·机器学习
冰冰菜的扣jio2 小时前
探秘数据库——MySQL基础(四)
数据库·mysql
愚公移码2 小时前
蓝凌EKP产品:扩展Druid 数据源KmssDruidDataSource在企业级数据源初始化与连接监控实践
数据库·hibernate·蓝凌·druiddatasource
电化学仪器白超2 小时前
20251209Ver8调试记录(补充电路板编号8-3)
python·单片机·嵌入式硬件·自动化
和光同尘20232 小时前
一文讲透CentOS下安装部署使用MYSQL
linux·运维·数据库·数据仓库·mysql·centos·database