Flask SQLAlchemy 教程:从零开始 – wiki基地


Flask SQLAlchemy 教程:从零开始构建一个数据驱动的Web应用

导言

欢迎来到本篇Flask SQLAlchemy的深度教程!如果你正在寻找如何将强大的数据库操作能力与轻量级的Flask Web框架结合,那么你来对地方了。本文将带你从零开始,一步步构建一个基于Flask和SQLAlchemy的数据驱动Web应用,深入理解ORM(对象关系映射)的概念和实践。

我们将构建一个简单的“图书馆管理系统”作为示例,涵盖数据库模型的定义、数据的创建、读取、更新、删除(CRUD)操作,以及模型之间关系的建立。

为什么选择 Flask 和 SQLAlchemy?

  • Flask: 一个轻量级的Web服务程序,被称为“微框架”。它不强制使用任何特定的工具或库,允许开发者自由选择组件。这使得Flask非常灵活,适合小型项目到中型项目的快速开发。
  • SQLAlchemy: Python中最流行的ORM和SQL工具包之一。它允许你使用Python对象而不是原始SQL语句来操作数据库。SQLAlchemy支持多种数据库(如SQLite, PostgreSQL, MySQL等),并且提供了强大的抽象层,使得数据库操作变得直观和高效。
  • Flask-SQLAlchemy: Flask的一个扩展,它简化了在Flask应用中集成SQLAlchemy的过程。它处理了许多配置和会话管理细节,让你能更专注于业务逻辑。

本教程将涵盖:

  1. 环境搭建与项目初始化
  2. Flask应用基础配置
  3. 集成Flask-SQLAlchemy
  4. 定义数据库模型 (ORM)
    • 基本字段类型
    • 主键、非空、唯一约束
    • 模型间的关系:一对多 (One-to-Many)
  5. 数据库初始化与迁移 (初探)
  6. CRUD (创建、读取、更新、删除) 操作详解
    • 创建数据 (Create)
    • 查询数据 (Read)
      • 查询所有
      • 按ID查询
      • 过滤与排序
      • 关联查询
    • 更新数据 (Update)
    • 删除数据 (Delete)
  7. 将CRUD操作集成到Web路由中
    • 使用表单处理用户输入 (简化版)
    • 渲染HTML模板展示数据
  8. 进阶概念与最佳实践
    • 数据库会话管理
    • 应用上下文与请求上下文
    • 错误处理
    • 项目结构建议
    • Flask-Migrate (Alembic) 简介
  9. 总结与展望

第一部分:环境搭建与项目初始化

在开始编写代码之前,我们需要设置好开发环境。

1. 创建项目目录和虚拟环境

虚拟环境是Python开发中的最佳实践,它能隔离项目依赖,避免不同项目间的包版本冲突。

“`bash

创建项目目录

mkdir flask_sqlalchemy_tutorial
cd flask_sqlalchemy_tutorial

创建并激活虚拟环境 (macOS/Linux)

python3 -m venv venv
source venv/bin/activate

创建并激活虚拟环境 (Windows)

python -m venv venv

.\venv\Scripts\activate

“`

激活虚拟环境后,你的命令行提示符前会显示 (venv),表示你当前正处于虚拟环境中。

2. 安装必要的库

我们将安装 FlaskFlask-SQLAlchemy

bash
pip install Flask Flask-SQLAlchemy

3. 项目文件结构

为了保持代码的组织性,我们先规划一个简单的项目结构:

flask_sqlalchemy_tutorial/
├── venv/ # 虚拟环境目录
├── app.py # Flask应用的主文件
├── models.py # 数据库模型定义文件
└── templates/ # HTML模板文件目录
├── index.html
├── authors.html
├── add_author.html
└── # ... 更多模板

第二部分:Flask 应用基础配置与集成 Flask-SQLAlchemy

现在,让我们来编写 app.py 的初始代码,并集成 Flask-SQLAlchemy。

1. app.py 基础结构

“`python

app.py

from flask import Flask, render_template, request, redirect, url_for
from flask_sqlalchemy import SQLAlchemy
import os # 用于构建文件路径,确保跨操作系统兼容性

初始化 Flask 应用

app = Flask(name)

配置数据库

这里我们使用SQLite数据库,文件名为 library.db,它会存储在项目根目录下

SQLALCHEMY_DATABASE_URI 是连接数据库的URI (统一资源标识符)

‘sqlite:///’ + os.path.join(app.root_path, ‘library.db’) 是一种更健壮的路径指定方式

它确保无论你在哪个目录下运行应用,数据库文件都会被创建在应用根目录

app.config[‘SQLALCHEMY_DATABASE_URI’] = ‘sqlite:///library.db’

如果你想使用内存数据库进行快速测试,可以设置为:

app.config[‘SQLALCHEMY_DATABASE_URI’] = ‘sqlite:///:memory:’

SQLALCHEMY_TRACK_MODIFICATIONS 配置项,用于设置是否追踪对象的修改并发送信号

如果设置为 True,会消耗额外内存。在大多数情况下,可以设置为 False,除非你需要此功能

app.config[‘SQLALCHEMY_TRACK_MODIFICATIONS’] = False

初始化 SQLAlchemy 实例

这样,db 对象就和我们的 Flask 应用绑定了

db = SQLAlchemy(app)

================================================================

以下是模型定义和路由,我们稍后会填充

================================================================

基本路由:首页

@app.route(‘/’)
def index():
return “

欢迎来到图书馆管理系统!

这是一个 Flask SQLAlchemy 教程的示例应用。

应用启动入口

if name == ‘main‘:
# 在应用首次运行时,创建数据库表
# 需要在应用上下文中执行 db.create_all()
# 否则会报错 “RuntimeError: Application context not pushed.
# Working outside of application context.”
with app.app_context():
db.create_all()
print(“数据库表已创建!”)
app.run(debug=True) # debug=True 会在代码修改后自动重启服务器,并提供调试信息
“`

代码解释:

  • from flask import Flask, ...: 导入Flask类和一些常用的函数,如 render_template(渲染HTML模板)、request(处理HTTP请求)、redirect(重定向)、url_for(构建URL)。
  • from flask_sqlalchemy import SQLAlchemy: 导入Flask-SQLAlchemy扩展。
  • app = Flask(__name__): 创建Flask应用实例。__name__ 是Python的内置变量,表示当前模块的名称。
  • app.config[...]: 配置Flask应用。
    • SQLALCHEMY_DATABASE_URI: 数据库连接字符串。sqlite:///library.db 表示使用SQLite数据库,数据文件名为 library.db,它将创建在应用运行的当前目录。os.path.join(app.root_path, 'library.db') 这种方式更为严谨,确保数据库文件在项目根目录。
    • SQLALCHEMY_TRACK_MODIFICATIONS: 设为 False 可以避免一些警告并节省资源,除非你确实需要跟踪修改。
  • db = SQLAlchemy(app): 初始化 SQLAlchemy 实例,并将其与我们的Flask应用 app 关联。这个 db 对象将是我们与数据库交互的主要接口。
  • @app.route('/'): 定义了一个路由,当用户访问应用的根URL时,index 函数会被调用。
  • if __name__ == '__main__':: 这是一个Python的标准写法,确保 app.run() 只在直接运行 app.py 时执行。
  • with app.app_context(): db.create_all(): 非常重要! db.create_all() 等数据库操作需要在Flask的“应用上下文”中执行。with app.app_context(): 提供了这个上下文。它会根据我们定义的模型,在数据库中创建相应的表。

现在,你可以运行 python app.py。如果你看到 “数据库表已创建!” 并且浏览器访问 http://127.0.0.1:5000/ 显示 “欢迎来到图书馆管理系统!”,那么恭喜你,基础环境已搭建成功!同时,你会在项目根目录看到一个 library.db 文件。

第三部分:定义数据库模型 (ORM)

ORM (Object-Relational Mapping) 是一种编程技术,它允许你使用面向对象的方式来操作数据库,而不是直接编写SQL语句。在Flask-SQLAlchemy中,你定义Python类来表示数据库表,类的属性表示表的列。

我们将定义两个模型:Author (作者) 和 Book (书籍),并建立它们之间的一对多关系(一个作者可以写多本书,一本书只能有一个作者)。

1. models.py 文件

为了更好的组织性,我们通常将数据库模型定义在一个单独的文件中,例如 models.py

“`python

models.py

from app import db # 从 app.py 中导入 db 实例

class Author(db.Model):
“””
作者模型
一个作者可以有多本书
“””
id = db.Column(db.Integer, primary_key=True) # 主键,整数类型,自动递增
name = db.Column(db.String(100), nullable=False, unique=True) # 姓名,字符串,非空,唯一

# 定义与 Book 模型的一对多关系
# 'Book' 是关联的模型的类名
# backref='author' 会在 Book 模型中添加一个 'author' 属性,指向对应的 Author 对象
# lazy=True 表示在访问 books 属性时才加载关联的 Book 对象,这是一种性能优化
books = db.relationship('Book', backref='author', lazy=True, cascade='all, delete-orphan')

def __repr__(self):
    # 定义对象的字符串表示,便于调试
    return f'<Author {self.name}>'

class Book(db.Model):
“””
书籍模型
一本书只能有一个作者
“””
id = db.Column(db.Integer, primary_key=True) # 主键
title = db.Column(db.String(200), nullable=False) # 书名,非空
year_published = db.Column(db.Integer) # 出版年份,整数类型
isbn = db.Column(db.String(13), unique=True, nullable=True) # ISBN号,可选,唯一

# 外键:关联到 Author 模型的 id
# 'author.id' 中的 'author' 是表名,而不是模型名(SQLAlchemy会自动将类名转换为小写表名)
author_id = db.Column(db.Integer, db.ForeignKey('author.id'), nullable=False)

def __repr__(self):
    return f'<Book {self.title} by {self.author.name if self.author else "Unknown"}>'

“`

代码解释:

  • from app import db: 我们从 app.py 中导入了 db 实例,因为模型需要使用它来定义列和关系。
  • class Author(db.Model):: 定义一个 Author 类,它继承自 db.Model。这告诉Flask-SQLAlchemy这是一个数据库模型,它将对应数据库中的一个表。
  • db.Column(...): 用于定义表的列。
    • db.Integer: 整数类型。
    • db.String(length): 字符串类型,必须指定最大长度。
    • primary_key=True: 将该列设为主键。主键通常是整数且自动递增,用于唯一标识每一行。
    • nullable=False: 表示该列不允许为 NULL (即必须有值)。
    • unique=True: 表示该列的值在表中必须是唯一的,不能重复。
  • db.relationship(...): 定义模型间的关系。
    • 'Book': 指向另一个模型的类名。
    • backref='author': 这是一个非常方便的参数。它会在 Book 模型中自动创建一个 author 属性,你可以通过 book_instance.author 来访问这本书的作者对象,而无需在 Book 模型中显式定义。
    • lazy=True: 决定何时加载关联对象。True (或 select) 表示在首次访问关联属性时才从数据库加载数据,这是一种延迟加载,可以提高查询效率。
    • cascade='all, delete-orphan': 这是一个高级参数,用于级联操作。delete-orphan 表示如果一个 Book 不再与任何 Author 关联(例如,手动将 book.author = None),那么这个 Book 对象会被自动删除。all 包含了 ‘save-update’, ‘merge’, ‘refresh-expire’, ‘expunge’, ‘delete’ 等操作,意味着父对象的所有操作会传递给子对象。这里是为了实现如果作者被删除,其所有书籍也一并删除。
  • db.ForeignKey('author.id'): 定义外键。它指向 Author 表的 id 列。注意这里用的是表名 author (通常是模型类名的小写形式),而不是模型名 Author
  • __repr__(self): 这是一个Python的特殊方法,当你打印一个模型对象时,它会返回一个友好的字符串表示,非常有助于调试。

2. 更新 app.py 导入模型

app.py 中,我们需要导入刚刚定义的模型,以便 db.create_all() 能发现它们并创建表。

修改 app.py 顶部:

“`python

app.py

from flask import Flask, render_template, request, redirect, url_for, flash
from flask_sqlalchemy import SQLAlchemy
import os
from datetime import datetime # 导入 datetime 模块,如果需要处理日期时间字段

从 models.py 中导入我们定义的 Author 和 Book 模型

from models import db, Author, Book # 注意这里是导入 db, Author, Book

… (其他配置保持不变) …

确保 db 实例在 app.py 中被创建,并传递给 models.py

(如果你的 models.py 也尝试创建 db 实例,那么会出错)

最好的做法是在 app.py 中创建 db 实例,然后在 models.py 中导入它

确保 app.py 和 models.py 的循环引用问题得到处理

当前的结构是 app.py 创建 db, models.py 导入 db,这是OK的。

… (routes 和 if name == ‘main‘: 部分) …

``
**注意:** 确保
models.py中的from app import db能够正确导入,这需要app.pymodels.py被导入之前已经创建了db实例。由于我们是在app.py中通过from models import db, Author, Book的方式导入模型,而models.py又需要db实例,所以models.py中的from app import db`是关键。

文件结构与导入顺序:
1. app.py 启动,创建 appdb 实例。
2. app.py 导入 models (from models import ...)。
3. models.py 执行,其中 from app import db 会从正在运行的 app.py 模块中获取已创建的 db 实例。
4. models.py 中定义 AuthorBook 模型,它们使用导入的 db 实例。
5. app.py 继续执行,有了 AuthorBook 模型类。
6. app.pyif __name__ == '__main__': 块中,with app.app_context(): db.create_all() 被执行,根据 AuthorBook 创建数据库表。

现在,再次运行 python app.py。如果你之前已经运行过,并且 library.db 存在,它可能不会重新创建表。为了确保新的模型结构生效,你可以删除 library.db 文件,然后再次运行。你会看到 AuthorBook 表在 library.db 中被创建。

第四部分:CRUD (创建、读取、更新、删除) 操作详解

现在我们有了模型和数据库表,是时候学习如何与数据进行交互了。所有的数据库操作都通过 db.session 进行。db.session 是一个数据库会话,它代表了一次与数据库的交互。

1. 创建数据 (Create)

创建数据涉及实例化你的模型类,然后将其添加到会话并提交。

“`python

假设你在 app.py 中的一个路由或 shell 环境中执行以下代码

1. 创建一个新的作者对象

new_author = Author(name=”三毛”)

2. 将对象添加到数据库会话

db.session.add(new_author)

3. 提交会话,将更改写入数据库

db.session.commit()

print(f”新作者 ‘{new_author.name}’ 已添加,ID: {new_author.id}”)

创建多本书籍并关联到作者

author_echo = Author.query.filter_by(name=”三毛”).first() # 查找刚刚添加的三毛

if author_echo:
book1 = Book(title=”撒哈拉的故事”, year_published=1976, author=author_echo)
book2 = Book(title=”雨季不再来”, year_published=1974, author=author_echo)
book3 = Book(title=”送你一匹马”, year_published=1983, author=author_echo)

db.session.add_all([book1, book2, book3]) # 添加多个对象
db.session.commit()
print(f"三毛的 {len(author_echo.books)} 本书已添加。")

else:
print(“未能找到作者 ‘三毛’。”)

你也可以先创建作者,再在Book中通过 author_id 关联

author_lu = Author(name=”鲁迅”)
db.session.add(author_lu)
db.session.commit()
book_aq = Book(title=”阿Q正传”, year_published=1921, author_id=author_lu.id)
db.session.add(book_aq)
db.session.commit()
print(f”鲁迅和 ‘{book_aq.title}’ 已添加。”)
“`

关键点:
* db.session.add(obj): 将一个新对象标记为待插入。
* db.session.add_all([obj1, obj2, ...]): 批量添加多个对象。
* db.session.commit(): 将所有待处理的更改(添加、更新、删除)写入数据库。这是一个原子操作,要么全部成功,要么全部回滚。

2. 读取数据 (Read)

读取数据是ORM最常用的功能之一,Flask-SQLAlchemy提供了强大的查询接口。每个模型类都有一个 .query 属性,用于构建查询。

“`python

查询所有作者

all_authors = Author.query.all()
print(“\n所有作者:”)
for author in all_authors:
print(f”- {author.name} (ID: {author.id})”)

按主键 (ID) 查询单个作者

author_by_id = Author.query.get(1) # get() 方法直接通过主键查询
if author_by_id:
print(f”\nID为1的作者: {author_by_id.name}”)
else:
print(“\n未找到ID为1的作者”)

查询第一个符合条件的作者 (例如,按名称)

first_author = Author.query.filter_by(name=”三毛”).first()
if first_author:
print(f”\n按名称查询到的第一个作者: {first_author.name}”)

查询所有三毛的书籍 (通过 Author 对象的 backref)

if first_author:
print(f”\n{first_author.name} 的书籍:”)
for book in first_author.books:
print(f” – {book.title} ({book.year_published})”)

查询所有书籍

all_books = Book.query.all()
print(“\n所有书籍:”)
for book in all_books:
print(f”- {book.title} by {book.author.name}”) # 通过 backref 访问作者

过滤查询:查询出版年份大于1975年的书籍

books_after_1975 = Book.query.filter(Book.year_published > 1975).all()
print(“\n1975年后出版的书籍:”)
for book in books_after_1975:
print(f”- {book.title} ({book.year_published})”)

过滤和排序:查询所有书籍,按书名升序排列

sorted_books = Book.query.order_by(Book.title.asc()).all()
print(“\n按书名排序的书籍:”)
for book in sorted_books:
print(f”- {book.title} by {book.author.name}”)

更复杂的过滤:查询作者名为“三毛”且出版年份大于1975年的书籍

complex_query = Book.query.join(Author).filter(Author.name == “三毛”, Book.year_published > 1975).all()
print(“\n作者为三毛且出版年份大于1975年的书籍:”)
for book in complex_query:
print(f”- {book.title} ({book.year_published}) by {book.author.name}”)

get_or_404 方法 (通常用于路由中,当找不到资源时自动返回404页面)

author_404 = Author.query.get_or_404(999) # 尝试访问不存在的ID,会抛出 HTTPException

“`

常用查询方法:
* Model.query.all(): 返回所有对象的列表。
* Model.query.first(): 返回查询到的第一个对象,如果没有则返回 None
* Model.query.get(id): 通过主键 id 直接获取对象。如果找不到,返回 None
* Model.query.get_or_404(id): 类似于 get(),但如果找不到对象,会立即终止请求并返回 HTTP 404 错误。非常适合在路由中使用。
* Model.query.filter_by(column=value, ...): 使用关键字参数进行等值过滤。
* Model.query.filter(Model.column > value, ...): 使用SQL表达式进行过滤,支持更复杂的条件 (如 > < != like in_)。
* Model.query.order_by(Model.column.asc()) / Model.column.desc(): 对结果进行排序。
* Model.query.limit(n) / Model.query.offset(n): 限制返回结果的数量和跳过的数量 (用于分页)。
* Model.query.join(OtherModel): 进行表连接,通常在涉及到关系查询时使用。

3. 更新数据 (Update)

更新数据涉及先查询到要修改的对象,然后修改其属性,最后提交会话。

“`python

1. 查询要更新的对象

author_to_update = Author.query.filter_by(name=”三毛”).first()

if author_to_update:
# 2. 修改对象的属性
print(f”\n正在更新作者:{author_to_update.name} 到 三毛 (原名)”)
author_to_update.name = “三毛 (原名)”

# 3. 提交会话,将更改写入数据库
db.session.commit()
print(f"作者已更新为: {author_to_update.name}")

# 更新一本书的年份
book_to_update = Book.query.filter_by(title="撒哈拉的故事").first()
if book_to_update:
    print(f"正在更新书籍 '{book_to_update.title}' 的出版年份从 {book_to_update.year_published} 到 1978")
    book_to_update.year_published = 1978
    db.session.commit()
    print(f"书籍 '{book_to_update.title}' 已更新。")

“`

关键点:
* 你不需要显式调用 db.session.add() 来更新一个已经从数据库中加载出来的对象。SQLAlchemy会自动追踪这些对象的修改。
* db.session.commit() 仍然是必须的,它会将这些修改持久化到数据库。

4. 删除数据 (Delete)

删除数据涉及先查询到要删除的对象,然后将其从会话中删除,最后提交会话。

“`python

1. 查询要删除的对象

author_to_delete = Author.query.filter_by(name=”鲁迅”).first()

if author_to_delete:
# 2. 将对象从数据库会话中标记为删除
print(f”\n正在删除作者: {author_to_delete.name}”)
db.session.delete(author_to_delete)

# 3. 提交会话,将更改写入数据库
db.session.commit()
print(f"作者 '{author_to_delete.name}' 已删除。")

# 验证是否删除
deleted_author_check = Author.query.filter_by(name="鲁迅").first()
if not deleted_author_check:
    print("已验证:鲁迅已从数据库中移除。")

# 由于我们在 Author 模型中设置了 cascade='all, delete-orphan',
# 当作者被删除时,其关联的书籍也会被删除。
# 我们可以尝试查询鲁迅的书籍来验证。
# (实际上,如果 cascade 设置正确,这里应该已经找不到关联的书籍了)

“`

关键点:
* db.session.delete(obj): 将一个对象标记为待删除。
* db.session.commit(): 将删除操作持久化到数据库。
* 级联删除 (cascade='all, delete-orphan') 在一对多关系中非常有用,可以自动处理父对象删除时子对象的命运。

第五部分:将CRUD操作集成到Web路由中

现在我们将结合Flask路由、HTML模板和数据库操作,构建一个更完整的Web应用。

首先,在项目根目录创建 templates 文件夹,并创建一些HTML文件。

1. templates/base.html (基础布局文件)

“`html






{% block title %}图书馆管理系统{% endblock %}


{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}

{{ message }}

{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}


“`

2. templates/index.html (首页)

“`html
{% extends “base.html” %}

{% block title %}首页 – 图书馆管理系统{% endblock %}

{% block content %}

欢迎来到图书馆管理系统!

这是一个使用 Flask 和 Flask-SQLAlchemy 构建的简单示例。

你可以通过导航栏来管理作者和书籍。

{% endblock %}
“`

3. templates/authors.html (作者列表)

“`html
{% extends “base.html” %}

{% block title %}作者列表{% endblock %}

{% block content %}

作者列表

{% if authors %}

{% else %}

暂无作者。

{% endif %}

添加新作者

{% endblock %}
“`

4. templates/add_author.html (添加/编辑作者表单)

“`html
{% extends “base.html” %}

{% block title %}{% if author %}编辑作者{% else %}添加作者{% endif %}{% endblock %}

{% block content %}

{% if author %}编辑作者: {{ author.name }}{% else %}添加新作者{% endif %}




返回作者列表

{% endblock %}
“`

5. templates/author_detail.html (作者详情)

“`html
{% extends “base.html” %}

{% block title %}作者详情 – {{ author.name }}{% endblock %}

{% block content %}

作者详情: {{ author.name }}

ID: {{ author.id }}

著作:

{% if author.books %}

{% else %}

这位作者还没有书籍。

{% endif %}

为 {{ author.name }} 添加书籍

编辑作者 | 返回作者列表

{% endblock %}
“`

6. templates/books.html (书籍列表)

“`html
{% extends “base.html” %}

{% block title %}书籍列表{% endblock %}

{% block content %}

书籍列表

{% if books %}

{% else %}

暂无书籍。

{% endif %}

添加新书籍

{% endblock %}
“`

7. templates/add_book.html (添加/编辑书籍表单)

“`html
{% extends “base.html” %}

{% block title %}{% if book %}编辑书籍{% else %}添加书籍{% endif %}{% endblock %}

{% block content %}

{% if book %}编辑书籍: {{ book.title }}{% else %}添加新书籍{% endif %}


    <label for="year_published">出版年份:</label>
    <input type="number" id="year_published" name="year_published" value="{{ book.year_published if book else '' }}" required>

    <label for="isbn">ISBN (可选):</label>
    <input type="text" id="isbn" name="isbn" value="{{ book.isbn if book else '' }}">

    <label for="author_id">作者:</label>
    <select id="author_id" name="author_id" required>
        {% for author_option in authors_for_select %}
            <option value="{{ author_option.id }}" {% if book and book.author_id == author_option.id %}selected{% endif %}>
                {{ author_option.name }}
            </option>
        {% endfor %}
    </select>
    <br><br>
    <button type="submit">{% if book %}更新书籍{% else %}添加书籍{% endif %}</button>
</form>
<p><a href="{{ url_for('list_books') }}">返回书籍列表</a></p>

{% endblock %}
“`

8. templates/book_detail.html (书籍详情)

“`html
{% extends “base.html” %}

{% block title %}书籍详情 – {{ book.title }}{% endblock %}

{% block content %}

书籍详情: {{ book.title }}

作者: {{ book.author.name }}

出版年份: {{ book.year_published }}

ISBN: {{ book.isbn if book.isbn else ‘无’ }}

ID: {{ book.id }}

编辑书籍 |
返回书籍列表

{% endblock %}
“`

9. 更新 app.py 中的路由

app.py 中添加以下路由:

“`python

app.py (接在 db = SQLAlchemy(app) 之后,if name == ‘main‘: 之前)

导入 flash,用于显示消息

from flask import Flask, render_template, request, redirect, url_for, flash

… (其他导入和配置)

作者管理路由 =============================================================

@app.route(‘/authors’)
def list_authors():
“””显示所有作者的列表”””
authors = Author.query.all()
return render_template(‘authors.html’, authors=authors)

@app.route(‘/author/add’, methods=[‘GET’, ‘POST’])
def add_author():
“””添加新作者”””
if request.method == ‘POST’:
author_name = request.form.get(‘name’)
if not author_name:
flash(‘作者姓名不能为空!’, ‘error’)
return redirect(url_for(‘add_author’))

    # 检查作者是否已存在
    existing_author = Author.query.filter_by(name=author_name).first()
    if existing_author:
        flash(f'作者 "{author_name}" 已存在!', 'warning')
        return redirect(url_for('add_author'))

    new_author = Author(name=author_name)
    try:
        db.session.add(new_author)
        db.session.commit()
        flash(f'作者 "{new_author.name}" 已成功添加!', 'success')
        return redirect(url_for('list_authors'))
    except Exception as e:
        db.session.rollback() # 出现错误时回滚事务
        flash(f'添加作者失败:{e}', 'error')
        return redirect(url_for('add_author'))
return render_template('add_author.html')

@app.route(‘/author/‘)
def view_author(author_id):
“””查看单个作者详情”””
author = Author.query.get_or_404(author_id)
return render_template(‘author_detail.html’, author=author)

@app.route(‘/author//edit’, methods=[‘GET’, ‘POST’])
def edit_author(author_id):
“””编辑作者信息”””
author = Author.query.get_or_404(author_id)
if request.method == ‘POST’:
new_name = request.form.get(‘name’)
if not new_name:
flash(‘作者姓名不能为空!’, ‘error’)
return redirect(url_for(‘edit_author’, author_id=author.id))

    # 检查新名称是否与现有其他作者重名
    existing_author_with_new_name = Author.query.filter(Author.name == new_name, Author.id != author_id).first()
    if existing_author_with_new_name:
        flash(f'作者 "{new_name}" 已存在,请使用其他名称!', 'warning')
        return redirect(url_for('edit_author', author_id=author.id))

    old_name = author.name
    author.name = new_name
    try:
        db.session.commit()
        flash(f'作者 "{old_name}" 已成功更新为 "{new_name}"!', 'success')
        return redirect(url_for('list_authors'))
    except Exception as e:
        db.session.rollback()
        flash(f'更新作者失败:{e}', 'error')
        return redirect(url_for('edit_author', author_id=author.id))
return render_template('add_author.html', author=author) # 复用添加作者的模板

@app.route(‘/author//delete’, methods=[‘POST’])
def delete_author(author_id):
“””删除作者”””
author = Author.query.get_or_404(author_id)
try:
author_name = author.name
db.session.delete(author)
db.session.commit()
flash(f’作者 “{author_name}” 及其所有书籍已成功删除!’, ‘success’)
except Exception as e:
db.session.rollback()
flash(f’删除作者失败:{e}’, ‘error’)
return redirect(url_for(‘list_authors’))

书籍管理路由 =============================================================

@app.route(‘/books’)
def list_books():
“””显示所有书籍的列表”””
books = Book.query.all()
return render_template(‘books.html’, books=books)

@app.route(‘/book/add’, methods=[‘GET’, ‘POST’])
@app.route(‘/author//add_book’, methods=[‘GET’, ‘POST’]) # 允许从作者详情页添加书籍
def add_book(author_id=None):
“””添加新书籍”””
authors = Author.query.order_by(Author.name).all() # 获取所有作者用于下拉选择
if request.method == ‘POST’:
title = request.form.get(‘title’)
year_published = request.form.get(‘year_published’)
isbn = request.form.get(‘isbn’)
selected_author_id = request.form.get(‘author_id’)

    if not title or not year_published or not selected_author_id:
        flash('书名、出版年份和作者都不能为空!', 'error')
        return redirect(request.url) # 重定向回当前页

    try:
        year_published = int(year_published)
        selected_author_id = int(selected_author_id)
    except ValueError:
        flash('出版年份和作者ID必须是有效的数字!', 'error')
        return redirect(request.url)

    author = Author.query.get(selected_author_id)
    if not author:
        flash('选择的作者不存在!', 'error')
        return redirect(request.url)

    # 检查书名和作者组合是否重复
    existing_book = Book.query.filter_by(title=title, author_id=selected_author_id).first()
    if existing_book:
        flash(f'书籍 "{title}" (作者: {author.name}) 已存在!', 'warning')
        return redirect(request.url)

    new_book = Book(title=title, year_published=year_published, isbn=isbn if isbn else None, author=author)

    try:
        db.session.add(new_book)
        db.session.commit()
        flash(f'书籍 "{new_book.title}" 已成功添加!', 'success')
        if author_id: # 如果是从作者详情页添加,则返回作者详情页
            return redirect(url_for('view_author', author_id=author_id))
        return redirect(url_for('list_books'))
    except Exception as e:
        db.session.rollback()
        flash(f'添加书籍失败:{e}', 'error')
        return redirect(request.url)
return render_template('add_book.html', authors_for_select=authors, author_id_preselected=author_id)

@app.route(‘/book/‘)
def view_book(book_id):
“””查看单个书籍详情”””
book = Book.query.get_or_404(book_id)
return render_template(‘book_detail.html’, book=book)

@app.route(‘/book//edit’, methods=[‘GET’, ‘POST’])
def edit_book(book_id):
“””编辑书籍信息”””
book = Book.query.get_or_404(book_id)
authors = Author.query.order_by(Author.name).all() # 获取所有作者用于下拉选择

if request.method == 'POST':
    title = request.form.get('title')
    year_published = request.form.get('year_published')
    isbn = request.form.get('isbn')
    selected_author_id = request.form.get('author_id')

    if not title or not year_published or not selected_author_id:
        flash('书名、出版年份和作者都不能为空!', 'error')
        return redirect(url_for('edit_book', book_id=book.id))

    try:
        year_published = int(year_published)
        selected_author_id = int(selected_author_id)
    except ValueError:
        flash('出版年份和作者ID必须是有效的数字!', 'error')
        return redirect(url_for('edit_book', book_id=book.id))

    author = Author.query.get(selected_author_id)
    if not author:
        flash('选择的作者不存在!', 'error')
        return redirect(url_for('edit_book', book_id=book.id))

    # 检查更新后的书名和作者组合是否与现有其他书籍重复
    existing_book_with_new_props = Book.query.filter(
        Book.title == title, 
        Book.author_id == selected_author_id,
        Book.id != book_id # 排除当前正在编辑的书籍
    ).first()
    if existing_book_with_new_props:
        flash(f'书籍 "{title}" (作者: {author.name}) 已存在!', 'warning')
        return redirect(url_for('edit_book', book_id=book.id))

    book.title = title
    book.year_published = year_published
    book.isbn = isbn if isbn else None
    book.author = author # 直接赋值 author 对象,SQLAlchemy会处理外键更新

    try:
        db.session.commit()
        flash(f'书籍 "{book.title}" 已成功更新!', 'success')
        return redirect(url_for('list_books'))
    except Exception as e:
        db.session.rollback()
        flash(f'更新书籍失败:{e}', 'error')
        return redirect(url_for('edit_book', book_id=book.id))

return render_template('add_book.html', book=book, authors_for_select=authors)

@app.route(‘/book//delete’, methods=[‘POST’])
def delete_book(book_id):
“””删除书籍”””
book = Book.query.get_or_404(book_id)
try:
book_title = book.title
db.session.delete(book)
db.session.commit()
flash(f’书籍 “{book_title}” 已成功删除!’, ‘success’)
except Exception as e:
db.session.rollback()
flash(f’删除书籍失败:{e}’, ‘error’)
return redirect(url_for(‘list_books’))

… (if name == ‘main‘: 部分保持不变) …

“`

运行应用:

  1. 确保你已激活虚拟环境。
  2. 确保 library.db 文件存在。如果之前已经运行过并且有数据,删除它并重新运行 python app.py 以获得一个干净的数据库。
  3. 运行 python app.py
  4. 在浏览器中访问 http://127.0.0.1:5000/。你可以通过导航栏来浏览、添加、编辑和删除作者和书籍。

代码亮点:

  • request.method: 用于判断HTTP请求是GET还是POST。
  • request.form.get('field_name'): 获取表单提交的数据。
  • render_template(): 渲染HTML模板,并可向模板传递变量。
  • redirect(url_for(...)): 重定向到另一个URL。url_for 是一个非常方便的函数,它根据视图函数名生成URL,避免硬编码URL。
  • flash(): Flask的内置消息闪现功能,用于在用户操作后显示临时消息(如成功提示或错误警告)。需要配合 get_flashed_messages() 在模板中显示。
  • try...except...db.session.rollback(): 这是处理数据库操作错误的关键模式。如果 db.session.commit() 失败(例如,违反唯一约束),rollback() 会撤销当前会话中的所有更改,防止数据不一致。
  • Author.query.get_or_404(author_id): 在根据ID查询对象时,如果对象不存在,直接返回404页面,简化了错误处理。
  • select 标签和 option 循环: 在添加/编辑书籍时,我们从数据库中加载所有作者,并将其渲染到HTML select 标签中,让用户可以选择作者。
  • 级联操作: 在删除作者时,因为我们在 Author 模型中设置了 cascade='all, delete-orphan',所有关联的书籍也会被自动删除。

第六部分:进阶概念与最佳实践

1. 数据库会话管理 (db.session)

  • db.session 代表着一个与数据库的事务性连接。所有数据库操作(添加、查询、修改、删除)都在这个会话中进行。
  • 自动管理: Flask-SQLAlchemy 会为每个请求自动创建一个会话,并在请求结束后(无论成功失败)自动关闭会话。这意味着你通常不需要手动调用 db.session.close()db.session.remove()
  • 事务性: db.session.commit() 会将会话中的所有更改作为单个原子操作提交。如果提交过程中发生任何错误,db.session.rollback() 可以撤销会话中的所有更改,保证数据的一致性。
  • 身份映射 (Identity Map): db.session 维护着一个身份映射,确保对于同一个数据库行,在同一个会话中只存在一个Python对象实例。这意味着你修改一个对象,即使这个对象是通过不同查询得到的,只要它在同一个会话中,它们都指向同一个内存对象。

2. 应用上下文与请求上下文

  • 应用上下文 (app.app_context()): 当你运行 flask run 或者在 if __name__ == '__main__': 块中执行 db.create_all() 时,需要一个应用上下文。这个上下文提供了应用级别的配置和资源。在路由函数中,Flask会自动为你处理应用上下文。
  • 请求上下文 (request_context()): 当一个HTTP请求到来时,Flask会创建一个请求上下文。这个上下文包含了 request 对象、session 对象等请求相关的特定数据。
  • 重要性: 许多Flask扩展,包括Flask-SQLAlchemy,都需要在正确的上下文中运行。如果你在路由之外尝试执行数据库操作,通常需要手动推入应用上下文。例如:
    python
    with app.app_context():
    # 这里可以安全地执行 db.session.query(...) 等操作
    authors_count = Author.query.count()
    print(f"当前有 {authors_count} 位作者。")

3. 错误处理

  • try...except...finally: 对于关键的数据库操作,总是建议使用 try...except 块。
    • try 块中执行操作和 db.session.commit()
    • except 块中捕获异常,并调用 db.session.rollback() 来回滚事务。
    • finally 块(可选)可以用于确保资源被清理,尽管Flask-SQLAlchemy通常会自动处理会话的关闭。
  • 用户友好反馈: 使用 flash() 函数向用户显示错误消息,而不是直接抛出异常。
  • get_or_404(): 查找特定ID的对象时,如果找不到,这个方法会直接返回一个HTTP 404错误,比手动检查 None 并返回错误页面更简洁。

4. 项目结构建议

随着项目增长,将所有代码放在 app.py 中会变得难以管理。建议将项目拆分为更小的模块:

flask_sqlalchemy_tutorial/
├── venv/
├── config.py # 配置信息 (数据库URI, SECRET_KEY 等)
├── run.py # 应用启动文件 (只包含 app.run() 和 db.create_all())
├── app/ # 主要应用目录
│ ├── __init__.py # 初始化 Flask 应用和 db 实例
│ ├── models.py # 数据库模型定义
│ ├── routes.py # 路由和视图函数
│ ├── forms.py # 表单定义 (如果使用 Flask-WTF)
│ └── templates/ # HTML模板
│ └── ...
└── migrations/ # 数据库迁移脚本 (如果使用 Flask-Migrate)
└── README.md

这种结构通过将各个功能模块化,提高了代码的可维护性和可扩展性。

5. 数据库迁移 (Flask-Migrate/Alembic)

在开发过程中,数据库模型可能会发生变化(例如,添加新列、修改列类型)。直接删除 library.db 文件重新创建表在开发阶段可行,但在生产环境中是不可接受的,因为它会丢失所有数据。

数据库迁移工具(如 Alembic,通过 Flask-Migrate 扩展集成)可以帮助你:
* 跟踪模型的变化。
* 自动生成数据库升级/降级脚本。
* 安全地在不丢失数据的情况下修改生产数据库结构。

简单介绍 Flask-Migrate:

  1. 安装: pip install Flask-Migrate
  2. 初始化:
    python
    # app.py 或 __init__.py
    from flask_migrate import Migrate
    # ...
    db = SQLAlchemy(app)
    migrate = Migrate(app, db) # 初始化 Migrate 实例

    在终端运行:flask db init (会在项目根目录创建 migrations 文件夹)
  3. 创建迁移: 当模型发生变化后:flask db migrate -m "Added ISBN to Book model" (会生成一个新的迁移脚本)
  4. 应用迁移: flask db upgrade (将数据库更新到最新状态)

在生产环境中,数据库迁移是必不可少的。

总结与展望

通过本教程,你已经掌握了使用 Flask 和 Flask-SQLAlchemy 从零开始构建一个数据驱动Web应用的核心知识:

  • 设置Flask和SQLAlchemy环境。
  • 定义复杂的数据库模型,包括字段类型、约束和一对多关系。
  • 执行完整的CRUD操作,实现数据的创建、读取、更新和删除。
  • 将这些操作集成到Flask路由中,并通过HTML模板与用户交互。
  • 了解了数据库会话、上下文管理、错误处理和项目结构等最佳实践。

这仅仅是开始。你可以继续探索以下高级主题:

  • 表单验证: 使用 Flask-WTF 来创建和验证表单,增强应用的健壮性。
  • 用户认证与授权: 结合 Flask-Login 实现用户注册、登录、会话管理和权限控制。
  • 蓝图 (Blueprints): 将应用的不同功能模块化,以便更好地组织大型应用。
  • API开发: 使用 Flask-RESTfulFlask-RESTX 构建RESTful API。
  • 部署: 将你的Flask应用部署到生产服务器(如Heroku, AWS, Dokku等)。
  • 测试: 编写单元测试和集成测试,确保代码的质量和稳定性。

希望这篇详细的教程能为你打下坚实的基础,让你在Flask和SQLAlchemy的开发之路上走得更远!祝你编程愉快!


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部