🧪 第2章 建立数据库应用程序
📄 一句话概括

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 秒实验

  1. 把地址里的斜杠改成两个,写成 sqlite://app.db,看它报什么。报错信息本身就带着线索。
  2. 在 create_all 之后给模型再加一个字段,再调一次 create_all,看表变了没有。
  3. 想一想:为什么 init_app 不能写在 models.py 里?

常见误区

  1. 把 db = SQLAlchemy() 写进工厂函数:每建一个应用就新建一个对象,模型绑不上它, 查询时报模型未注册。
  2. 拿 create_all 当迁移用:加字段时库和代码对不上,一查就报 no such column。
  3. 地址写相对路径又换了工作目录:库文件建在奇怪的地方,下次启动看到的是一个空库。
  4. 以为 Flask-SQLAlchemy 就是 SQLAlchemy:查文档查到 ORM 那一层,配置项名字全对不上。
自测三问(点开对答案) 1. db 对象为什么建在模块里而不是工厂里? —— models.py 要 import 它,写进工厂就会和 models 互相 import。 2. create_all 和迁移最大的区别? —— create_all 只补缺的表,不改已有表的结构。 3. 哪种地址错误最难查? —— 路径不通那种:启动正常,第一次查库才报错。

接好线了,下一篇定义模型,然后走一遍迁移三件套。

相关笔记