一个继承 db.Model 的类就是一张表,类属性就是列,类名转小写就是表名。 迁移三件套:init 建目录、migrate 生成脚本、upgrade 应用到库。三件事分得很清。 实测加一个 age 字段,create_all 完全不管,migrate 加 upgrade 一步到位。
这一篇对应教材 2.4 到 2.4.2:定义模型、数据库的初始化与迁移。 这是本章唯一标三颗星的一篇,因为迁移这套东西不自己跑一遍是记不住的。
一、一个类就是一张表
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(20), nullable=False)
email = db.Column(db.String(60), unique=True)
类名 User 转成小写就是表名 user。想换名字就自己写 __tablename__。
实测:表名 user,主键是 id,三列的类型分别是 INTEGER、VARCHAR(20)、VARCHAR(60)。
注意 nullable:写了 nullable=False 的那列是 False,
没写的 email 是 True——不写就是允许空,这个默认值很反直觉。
表名不是猜出来的。一旦库建好了再改类名,表名跟着变,查询立刻报 no such table。
二、列类型怎么挑
| 写法 | SQLite 里的类型 | 什么时候用 |
|---|---|---|
| db.Integer | INTEGER | 整数、主键 |
| db.String(20) | VARCHAR(20) | 短文本,要给长度 |
| db.Text | TEXT | 长文本,不给长度 |
| db.Boolean | BOOLEAN | 存成 0 与 1 |
| db.DateTime | DATETIME | 配 default 用 |
String 那个长度不是提示,是真限制。
MySQL 上超了直接报错,SQLite 上宽松得多——这也是本地跑得好、上线就崩的一个经典来源。
unique=True 会在建表时生成唯一索引。
实测撞上去报的是UNIQUE constraint failed: user.email,句子里带着表名和列名,很好认。
default=0 给的是 SQLAlchemy 插入时补的值,不是数据库层的 DEFAULT。
绕过 ORM 直接写 SQL 就不会被补上。
三、迁移三件套各干什么
| 命令 | 干什么 | 跑几次 |
|---|---|---|
| flask db init | 建 migrations 目录 | 一次 |
| flask db migrate -m 说明 | 对比模型和库,生成脚本 | 每次改模型 |
| flask db upgrade | 把脚本应用到库 | 每次拿到新脚本 |
实测三条命令返回码都是 0。 migrate 那一步的输出里有一句Detected added table 'user'——看到这句才算「差异找到了」。 upgrade 那一步打的是 Running upgrade 加一串哈希,再跟一句说明。 migrate 不碰库。它只写文件。忘了 upgrade,库一点没变,这是新手最常卡住的地方。
四、库里真的多出了什么
实测 upgrade 之后库里两张表:alembic_version 和 user。 alembic_version 只有一行,存的是当前版本的哈希。它决定下一次 upgrade 从哪开始。 migrations/versions 目录下多了一个脚本,文件名是哈希加说明。
这个哈希每次生成都不一样,因为里面带了当时的路径与时间。 所以正文里不写具体值,你跑出来的必然和这里不同。
五、改模型之后再迁移一次
给 models.py 加一行 age = db.Column(db.Integer, default=0),再跑 migrate 和 upgrade。 实测列从 id、name、email 变成 id、name、email、age。 这一条是 create_all 做不到的。同样的情况下 create_all 什么也不做, 然后查询一跑就报 no such column。
线上出这个错很难看:表结构落后于代码,所有用到新字段的页面一起 500, 而日志里只有一句 no such column。
六、脚本进仓库,库文件不进
migrations 目录要提交到 git,数据库文件不要。 理由是两类东西的性质不同:脚本是「怎么变成这个结构」的记录,可以重放; 库文件是「现在有什么数据」,因人而异。 别人拉下来跑一次 flask db upgrade,就有和你一样的表结构。
这条在团队里是硬性的。把 .db 提交上去,两个人各改一版,冲突根本没法合。
七、动手 Demo:定义模型、初始化与迁移
这个 Demo 从一张 user 表开始,走完整的迁移三件套,再改模型走第二遍。 每一节都直接打开 sqlite 文件看真东西,不靠猜。
# 1. 建 migrations 目录(一辈子一次)
flask db init
# 2. 对比模型和库,生成脚本(不碰库)
flask db migrate -m "建 user 表"
# 3. 把脚本应用到库
flask db upgrade
实跑输出:
--------------------------------------------------------------------
定义模型、初始化与迁移
--------------------------------------------------------------------
① 模型长什么样
--------------------------------------------------------------------
[代码原样]
| class User(db.Model):
| id = db.Column(db.Integer, primary_key=True)
| name = db.Column(db.String(20), nullable=False)
| email = db.Column(db.String(60), unique=True)
类名 User 自动映射成表名 user(snake_case,小写)。
表名 -> user
主键 -> ['id']
列 -> [('id', 'INTEGER', False), ('name', 'VARCHAR(20)', False), ('email', 'VARCHAR(60)', True)]
! 表名不是猜的:__tablename__ 要么写死,要么由类名转小写。
一旦库建好了再改类名,表名跟着变,查询立刻找不到表。
② 走一遍迁移三件套
--------------------------------------------------------------------
flask db init -> 0 (建 migrations 目录,只跑一次)
Creating directory .\migrations ... done
flask db migrate -m 建 user 表 -> 0 (对比模型和库,生成差异脚本)
INFO [alembic.autogenerate.compare.tables] Detected added table 'user'
flask db upgrade -> 0 (把脚本应用到库)
INFO [alembic.runtime.migration] Running upgrade -> 7fe4b2904ead, 建 user 表
! 三条命令的分工要分清:init 只做目录初始化,一辈子跑一次;migrate 只生成脚本、不碰库;
upgrade 才真正改库。改了模型忘了 migrate,upgrade 什么也不会做。
③ 库里真的多出了什么
--------------------------------------------------------------------
表 -> ['alembic_version', 'user']
user 列 -> [('id', 'INTEGER'), ('name', 'VARCHAR(20)'), ('email', 'VARCHAR(60)')]
alembic_version 里记着当前版本 -> [('7fe4b2904ead',)]
versions 目录下有 1 个脚本:7fe4b2904ead_建_user_表.py
! 迁移脚本要进仓库,数据库文件不进。别人拉下来跑一次 upgrade 就有同样的库。
④ 改模型之后再迁移一次
--------------------------------------------------------------------
改了 models.py 再 migrate -> 0
再 upgrade -> 0
现在 user 的列 -> ['id', 'name', 'email', 'age']
! 这一条是 create_all 做不到的。
加字段必须走迁移,否则线上表结构和代码对不上,查询一跑就报「no such column」。
第二节那三行返回码全是 0。命令跑成功和「库真的变了」是两件事,所以第三节一定要自己打开库看。 哈希每次都不一样,别照抄正文里的值。 第四节才是重点:同一个加字段的动作,create_all 什么都不做,迁移做对了。
30 秒实验
- 把 User 类的
__tablename__改成 members,跑一遍 migrate 与 upgrade, 看生成的是改名还是删表建表。 - 手动删掉 alembic_version 里那一行,再 upgrade 一次,看会发生什么。
- 想一想:为什么 init 只要跑一次,而 migrate 要每改一次模型就跑一次?
常见误区
- 改了模型只跑 migrate 不跑 upgrade:脚本生成了,库一点没变,然后以为是模型写错了。
- 用 create_all 代替迁移加字段:查询报 no such column,而且这个错只在碰新字段的页面上出现。
- 把 .db 文件提交进仓库:两个人各改一版,二进制冲突无法合并。
- 每次改模型都重新 init:报目录已存在,然后真以为迁移坏了。
自测三问(点开对答案)
1. 三件套里哪一步会真正改库? —— upgrade。init 只建目录,migrate 只生成脚本,都不碰库。 2. alembic_version 有什么用? —— 记当前版本,决定下一次 upgrade 从哪个脚本开始。 3. create_all 与迁移的根本区别? —— create_all 只补缺的表,不改已有表的结构。表有了,下一篇写增删改查,顺带把事务那三个动作分清楚。
- ☐ (本篇完,下一篇 2.5 SQLAlchemy 基本数据操作)