Flask 自己不碰数据库,接线的是 Flask-SQLAlchemy:一个 db 对象,一个地址。 对象在模块层建、绑定放在工厂里,这样 models.py 才不用反过来 import 应用。 地址写错有三种,报错时机完全不同:驱动缺失当场炸,路径不通要等第一次查库。
这一篇对应教材 2.3 节:安装扩充功能、flask-sqlalchemy 与 flask-migrate 的使用准备。 上一篇的骨架里已经留了 models.py,这一篇把 db 放进去。
一、先装两个扩展
教材 2.3.1 让装两个:flask-sqlalchemy 管模型与查询,flask-migrate 管改表结构。
pip install flask-sqlalchemy flask-migrate
装完会连带装进来 SQLAlchemy 与 Alembic 两个底层库。前者是 ORM 本身,后者是迁移引擎。 本沙箱实测的版本:Flask-SQLAlchemy 3.1.1、SQLAlchemy 2.1.6、Alembic 1.20.1。 这两个不要混。SQLAlchemy 是 ORM, Flask-SQLAlchemy 只是把它接到 Flask 上的一层薄壳——查文档时找错地方,会看到一堆用不上的配置项。
二、接入的两步
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy() # 在模块层建对象
def create_app():
app = Flask(__name__)
db.init_app(app) # 在工厂里绑定应用
return app
分成两步,理由只有一个:models.py 要 import db,工厂要 import models。写在一起就是循环导入。 所以对象在模块层建,绑定(init_app)放在工厂里。这是所有 Flask 扩展的共同模式。 记住这个形状:Flask-Login、Flask-WTF、Flask-Migrate 全是这两步,换的只是类名。
三、地址里那几个斜杠
地址写成 sqlite:/// 加路径。三个斜杠是 SQLite 的固定写法,后面跟什么就是什么。 跟相对路径,库文件相对当前工作目录;跟绝对路径,位置固定。 实测给的是带盘符的绝对路径,所以库文件稳定落在工程目录里,不会跟着启动位置跑。
换 MySQL 时地址变成 mysql+pymysql://用户:密码@主机/库名。加号后面是驱动名。 教材用的是这个写法。它和 mysql:// 的区别就在那个加号—— 没有加号时 SQLAlchemy 会去找 MySQLdb 这个老驱动。
四、create_all 与迁移的分工
db.create_all() 会照着模型建表,但只建「库里没有的表」。
实测调用之后库里多出一张 user 表,三列 id、name、email,类型和代码里写的一致。
加一个字段它不管。这是它和迁移最本质的区别。
所以 create_all 只在两种场合用:本地临时试验、给测试造一套干净的库。正式改表结构一律走迁移。
| create_all | 迁移 | |
|---|---|---|
| 建缺的表 | 会 | 会 |
| 给已有表加列 | 不管 | 会 |
| 留历史记录 | 没有 | 记在 alembic_version |
| 能回滚 | 不能 | 能 |
五、地址写错的三种报错
实测了三种写错法,报错的时机完全不同,这决定了你该往哪看。 第一种,前缀认得但驱动没装。 比如写 mysql:// 而环境里没有 MySQLdb,create_app 里就抛 ModuleNotFoundError。 第二种,前缀对、目录不存在。 启动一切正常,第一次查库才抛OperationalError: unable to open database file。
第三种,干脆没给地址。create_app 里直接抛 RuntimeError,说 URI 或 BINDS 必须有一个。 最坑的是第二种。启动不报错,服务能起来, 第一个真实请求才炸——而那时你已经以为是业务代码的问题了。 所以看到 OperationalError,先回头看地址和路径,别在代码里找。
六、动手 Demo:设置 SQLAlchemy:一个对象,一个地址
这个 Demo 在上一篇的骨架上接数据库:建对象、绑定、看地址、建表。 最后一节故意写错三次地址,把三种报错时机量出来。
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
def create_app():
app = Flask(__name__)
app.config.from_mapping(
SQLALCHEMY_DATABASE_URI="sqlite:///app.db",
)
db.init_app(app)
return app
实跑输出:
--------------------------------------------------------------------
设置 SQLAlchemy:一个对象,一个地址
--------------------------------------------------------------------
① 三行接入
--------------------------------------------------------------------
[代码原样]
| from flask import Flask
| from flask_sqlalchemy import SQLAlchemy
|
| db = SQLAlchemy() # 在模块层建对象
|
| def create_app():
| app = Flask(__name__)
| db.init_app(app) # 在工厂里绑定应用
| return app
分成两步是为了让 models.py 能只 import db,不 import app——否则循环导入必然发生。
这是 Flask 插件通用的模式:对象在模块层建,绑定放在工厂里。
② 地址与引擎
--------------------------------------------------------------------
app.config 里的地址 -> sqlite:///I:/Desktop/自我教学/.workbuddy/scripts/ch62_dev/_tmp62/db/app.db
db.engine.url.database -> I:/Desktop/自我教学/.workbuddy/scripts/ch62_dev/_tmp62/db/app.db
数据库版本 -> 3.53.1
! 三个斜杠是 SQLite 的固定写法:sqlite:/// 后面跟文件名。
跟相对路径就相对当前工作目录,跟绝对路径就固定位置——本沙箱给的是带盘符的绝对路径,
所以库文件稳定落在工程目录里。
③ 建表:create_all 与迁移的分工
--------------------------------------------------------------------
create_all 之后库里的表 -> ['user']
user 表的列 -> [('id', 'INTEGER'), ('name', 'VARCHAR(20)'), ('email', 'VARCHAR(60)')]
! create_all 只会建「库里没有的表」,不会改已有的表。加一个字段它不管——所以真项目要靠迁移。
④ 地址写错,什么时候才发现
--------------------------------------------------------------------
1)前缀认得,但没装驱动 -> create_app 就报
ModuleNotFoundError: No module named 'MySQLdb'
2)前缀对,目录不存在 -> 第一次查询才报
OperationalError: (sqlite3.OperationalError) unable to open database file
3)干脆没给地址 -> create_app 就报
RuntimeError: Either 'SQLALCHEMY_DATABASE_URI' or 'SQLALCHEMY_BINDS' must be set.
! 最坑的是第二种:启动一切正常,第一次查库才炸。
看到 OperationalError 先回头看地址和路径,别在代码里找。
! 第一种的报错里那句 No module named 就是线索——SQLAlchemy 按前缀挑驱动,
mysql:// 要的是 MySQLdb。想用纯 Python 的 pymysql 得写成 mysql+pymysql://,教材里就是这种写法。
! 本沙箱是 Flask-SQLAlchemy 3.1.1:没给地址会在 create_app 里直接抛 RuntimeError,
不会拖到你发请求。
第二节那两行是同一个路径:配置里存的是它,引擎解析出来的 database 也是它。 数据库版本 3.53.1 是沙箱里 SQLite 的版本,不是 SQLAlchemy 的版本。 第四节那三条是本篇最值钱的东西:同一个「写错了」,三个完全不同的报错位置。
30 秒实验
- 把地址里的斜杠改成两个,写成 sqlite://app.db,看它报什么。报错信息本身就带着线索。
- 在 create_all 之后给模型再加一个字段,再调一次 create_all,看表变了没有。
- 想一想:为什么 init_app 不能写在 models.py 里?
常见误区
- 把
db = SQLAlchemy()写进工厂函数:每建一个应用就新建一个对象,模型绑不上它, 查询时报模型未注册。 - 拿 create_all 当迁移用:加字段时库和代码对不上,一查就报 no such column。
- 地址写相对路径又换了工作目录:库文件建在奇怪的地方,下次启动看到的是一个空库。
- 以为 Flask-SQLAlchemy 就是 SQLAlchemy:查文档查到 ORM 那一层,配置项名字全对不上。
自测三问(点开对答案)
1. db 对象为什么建在模块里而不是工厂里? —— models.py 要 import 它,写进工厂就会和 models 互相 import。 2. create_all 和迁移最大的区别? —— create_all 只补缺的表,不改已有表的结构。 3. 哪种地址错误最难查? —— 路径不通那种:启动正常,第一次查库才报错。接好线了,下一篇定义模型,然后走一遍迁移三件套。
- ☐ (本篇完,下一篇 2.4 定义模型、初始化与迁移)