bigywater.tech
Flask · 97

Flask-Migrate Migration 文件

一个迁移文件里到底有什么?自动生成能信几成?数据迁移怎么写?想先看 SQL 再执行怎么办?这篇每种改动都从同一个基线项目出发,用真实的 flask db 命令逐个验证。

server_default
这类改动 autogenerate 默认检测不到
删 + 加
改列名被自动识别成的操作,老数据会丢
--sql
离线模式只打印 SQL、不连接数据库

迁移文件的结构

实测 · 本机执行输出
  文件名: r001_create_user_table.py (-m "create user table" 变成文件名里的小写下划线;--rev-id r001 指定了 revision id)
  文件头部:
    """create user table
    
    Revision ID: r001
    Revises: 
    Create Date: 2026-10-02 12:30:38.222633
    
    """
    from alembic import op
    import sqlalchemy as sa
    
    
    # revision identifiers, used by Alembic.
    revision = 'r001'
    down_revision = None
    branch_labels = None
    depends_on = None
  文件里的函数: ['upgrade', 'downgrade']

上一篇:SQLAlchemy 查询日志;下一篇:数据迁移回滚。

autogenerate 的能力

实测 · 本机执行输出
  [加一列 age] 检测到: Detected added column 'user.age'
  [改列长度 name 50→80] 检测到: Detected type change from VARCHAR(length=50) to String(length=80) on 'user.name'
  [改 nullable(email 不可空)] 检测到: Detected NOT NULL on column 'user.email'
  [改 server_default 的值] 没有检测到任何变化(没有生成迁移)
  [给 email 加索引] 检测到: Detected added index 'ix_user_email' on '('email',)'
  [给 email 加 unique=True] 检测到: Detected added unique constraint None on '('email',)'
  [把 email 改名为 mail] 检测到: Detected added column 'user.mail'; Detected removed column 'user.email'
  [删掉 email 列] 检测到: Detected removed column 'user.email'

第 74 篇看过 init、migrate、upgrade 的基本流程,这一篇看文件内容与检测边界。

没有名字的约束

实测 · 本机执行输出
  生成的迁移里的操作: ["batch_op.create_unique_constraint(None, ['email'])"]
  upgrade → 失败:ValueError: Constraint must have a name

手写数据迁移

实测 · 本机执行输出
  空脚本里原来是 pass;手写的两个函数:
    def upgrade():
        op.execute("update user set email = name || '@example.com' where email = 'none'")
    def downgrade():
        op.execute("update user set email = 'none' where email like '%@example.com'")
  执行前: [('bob', 'none')]
  upgrade 后: [('bob', 'bob@example.com')] | 退出码 0
  downgrade 后: [('bob', 'none')] | 退出码 0

离线 SQL

实测 · 本机执行输出
  upgrade r001:r900 --sql 的输出:
     -- Running upgrade r001 -> r900
     update user set email = name || '@example.com' where email = 'none';
     UPDATE alembic_version SET version_num='r900' WHERE alembic_version.version_num = 'r001';
  数据库的版本号前后: [('r001',)] → [('r001',)] (没变)

stamp

实测 · 本机执行输出
  stamp r777 之后 alembic_version = [('r777',)] (没有执行 r777 的任何操作,只是记账)
  flask db current: ['r777 (head)']

分步演示

迁移文件
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.7 / Flask 3.1.3 / Werkzeug 3.1.9 / Jinja2 3.1.6 / SQLAlchemy 2.1.1 / gunicorn 26.2.0(都装在自己的虚拟环境里);大多数实验用 Flask 的 test_client(直接调用 WSGI 应用,不经过网络),需要真实 HTTP 时另起 gunicorn 或开发服务器(端口 9600 以上,本机 127.0.0.1);计时用 time.perf_counter,数字每次运行会有小幅抖动。Flask-Migrate 4.1.0(Alembic),SQLite(render_as_batch=True)
    • 没有验证:PostgreSQL / MySQL 上同样改动的检测结果与执行结果、compare_server_default=True 打开之后能否检测、命名约定(naming_convention)、多个 head 的合并(见第 74 篇)、大表上的锁与耗时
    京ICP备2026067502号-1京公网安备11010502063487号