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的过程。它处理了许多配置和会话管理细节,让你能更专注于业务逻辑。
本教程将涵盖:
- 环境搭建与项目初始化
- Flask应用基础配置
- 集成Flask-SQLAlchemy
- 定义数据库模型 (ORM)
- 基本字段类型
- 主键、非空、唯一约束
- 模型间的关系:一对多 (One-to-Many)
- 数据库初始化与迁移 (初探)
- CRUD (创建、读取、更新、删除) 操作详解
- 创建数据 (Create)
- 查询数据 (Read)
- 查询所有
- 按ID查询
- 过滤与排序
- 关联查询
- 更新数据 (Update)
- 删除数据 (Delete)
- 将CRUD操作集成到Web路由中
- 使用表单处理用户输入 (简化版)
- 渲染HTML模板展示数据
- 进阶概念与最佳实践
- 数据库会话管理
- 应用上下文与请求上下文
- 错误处理
- 项目结构建议
- Flask-Migrate (Alembic) 简介
- 总结与展望
第一部分:环境搭建与项目初始化
在开始编写代码之前,我们需要设置好开发环境。
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. 安装必要的库
我们将安装 Flask 和 Flask-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.py在models.py被导入之前已经创建了db实例。由于我们是在app.py中通过from models import db, Author, Book的方式导入模型,而models.py又需要db实例,所以models.py中的from app import db`是关键。
文件结构与导入顺序:
1. app.py 启动,创建 app 和 db 实例。
2. app.py 导入 models (from models import ...)。
3. models.py 执行,其中 from app import db 会从正在运行的 app.py 模块中获取已创建的 db 实例。
4. models.py 中定义 Author 和 Book 模型,它们使用导入的 db 实例。
5. app.py 继续执行,有了 Author 和 Book 模型类。
6. app.py 的 if __name__ == '__main__': 块中,with app.app_context(): db.create_all() 被执行,根据 Author 和 Book 创建数据库表。
现在,再次运行 python app.py。如果你之前已经运行过,并且 library.db 存在,它可能不会重新创建表。为了确保新的模型结构生效,你可以删除 library.db 文件,然后再次运行。你会看到 Author 和 Book 表在 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
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
{% 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 %}
-
{% for author in authors %}
- {{ author.name }}
{% endfor %}
{% 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 %}
-
{% for book in author.books %}
- {{ book.title }} ({{ book.year_published }})
{% endfor %}
{% else %}
这位作者还没有书籍。
{% endif %}
{% endblock %}
“`
6. templates/books.html (书籍列表)
“`html
{% extends “base.html” %}
{% block title %}书籍列表{% endblock %}
{% block content %}
书籍列表
{% if books %}
-
{% for book in books %}
- {{ book.title }} ({{ book.year_published }}) by {{ book.author.name }}
{% endfor %}
{% else %}
暂无书籍。
{% endif %}
{% endblock %}
“`
7. templates/add_book.html (添加/编辑书籍表单)
“`html
{% extends “base.html” %}
{% block title %}{% if book %}编辑书籍{% else %}添加书籍{% endif %}{% endblock %}
{% block content %}