Alembic 教程:创建、管理与执行数据库迁移

Alembic 以 SQLAlchemy 为底层引擎,为关系数据库提供变更管理脚本的创建、管理和执行功能。本教程完整介绍这项工具的原理与用法。

开始前,请确认已经安装 Alembic。安装章节介绍了在本地虚拟环境中安装的常见方式。如该章节所示,通常应将 Alembic 安装到与目标项目相同的模块路径/Python 路径中,一般使用 Python 虚拟环境。这样运行 alembic 命令时,由它调用的 Python 脚本——项目中的 env.py——就能访问应用的模型。这并非严格要求,但通常是更合适的选择。

下文假定 alembic 命令行工具位于本地 PATH 中,并且被调用时能够访问与目标项目相同的 Python 模块环境。

迁移环境

使用 Alembic 的第一步是创建迁移环境(Migration Environment)。它是专属于某个应用的一组脚本目录。迁移环境只创建一次,随后与应用源代码一起维护。通过 Alembic 的 init 命令创建后,还可以按应用的具体需要定制。

包括一些已生成迁移脚本在内,环境结构如下:

yourproject/
    alembic.ini
    pyproject.toml
    alembic/
        env.py
        README
        script.py.mako
        versions/
            3512b954651e_add_account.py
            2b1ae634e5cd_add_order_id.py
            3adcc9a56557_rename_username_field.py

该目录包含以下目录和文件:

  • alembic.ini:Alembic 主配置文件,所有模板都会生成它。后面的编辑 .ini 文件一节会详细说明。
  • pyproject.toml:多数现代 Python 项目都有此文件。Alembic 也可以选择在其中存放与项目有关的配置。使用方法见使用 pyproject.toml 配置。
  • yourproject:应用源代码的根目录,或者根目录中的某个目录。
  • alembic:位于应用源代码树内,是迁移环境所在目录。它可以使用任意名称;使用多个数据库的项目甚至可以有多个这样的目录。
  • env.py:每次调用 alembic 迁移工具时运行的 Python 脚本。它至少包含配置并生成 SQLAlchemy 引擎、从引擎获取连接和事务,以及使用该连接作为数据库连接来源来调用迁移引擎的指令。

    将 env.py 作为生成环境的一部分,是为了让迁移执行方式能够完全定制。连接的具体方式以及调用迁移环境的细节都在这里。可以修改脚本,使其操作多个引擎、向迁移环境传入自定义参数,或加载并提供应用特定的库和模型。

    Alembic 提供一组初始化模板,为不同用例提供不同形式的 env.py。

  • README:各种环境模板中都带有该文件,应该包含一些说明性内容。
  • script.py.mako:用于生成新迁移脚本的 Mako 模板文件。其内容会用于生成 versions/ 内的新文件。模板可以编程定制,从而控制每个迁移文件的结构,例如统一导入哪些模块,或改变 upgrade()、downgrade() 函数结构。举例说,multidb 环境可以按 upgrade_engine1()、upgrade_engine2() 这样的命名方式生成多个函数。
  • versions/:存放各个版本脚本。用过其他迁移工具的读者可能会发现,这些文件不用递增整数编号,而采用部分 GUID。Alembic 依据脚本内部指令确定版本脚本顺序。理论上可以将一个版本文件“插入”其他文件之间,以合并不同分支的迁移序列,不过必须谨慎地手工操作。

创建环境

了解环境的基本概念后,可以使用 alembic init 创建它。以下命令用 generic 模板创建环境:

$ cd /path/to/yourproject
$ source /path/to/yourproject/.venv/bin/activate   # assuming a local virtualenv
$ alembic init alembic

上面的 init 命令生成名为 alembic 的迁移目录:

Creating directory /path/to/yourproject/alembic...done
Creating directory /path/to/yourproject/alembic/versions...done
Generating /path/to/yourproject/alembic.ini...done
Generating /path/to/yourproject/alembic/env.py...done
Generating /path/to/yourproject/alembic/README...done
Generating /path/to/yourproject/alembic/script.py.mako...done
Please edit configuration/connection/logging settings in
'/path/to/yourproject/alembic.ini' before proceeding.

该布局由名为 generic 的布局模板生成。Alembic 还包括其他环境模板,可用 list_templates 命令列出:

$ alembic list_templates
Available templates:

generic - Generic single-database configuration.
pyproject - pep-621 compliant configuration that includes pyproject.toml
async - Generic single-database configuration with an async dbapi.
multidb - Rudimentary multi-database configuration.

Templates are used via the 'init' command, e.g.:

  alembic init --template generic ./scripts

编辑 .ini 文件

Alembic 在当前目录中放置了 alembic.ini 文件。运行其他命令时,它会在当前目录查找此文件。要指定其他位置,可以使用 --config 选项,或设置 ALEMBIC_CONFIG 环境变量。

generic 创建的一体化 .ini 文件如下:

# A generic, single database configuration.

[alembic]
# path to migration scripts.
# this is typically a path given in POSIX (e.g. forward slashes)
# format, relative to the token %(here)s which refers to the location of this
# ini file
script_location = %(here)s/alembic

# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# Or organize into date-based subdirectories (requires recursive_version_locations = true)
# file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s

# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
prepend_sys_path = .

# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library.
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
# string value is passed to ZoneInfo()
# leave blank for localtime
# timezone =

# max length of characters to apply to the
# "slug" field
# truncate_slug_length = 40

# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false

# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false

# version location specification; This defaults
# to <script_location>/versions.  When using multiple version
# directories, initial revisions must be specified with --version-path.
# the special token `%(here)s` is available which indicates the absolute path
# to this configuration file.
#
# The path separator used here should be the separator specified by "version_path_separator" below.
# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions

# path_separator (New in Alembic 1.16.0, supersedes version_path_separator);
# This indicates what character is used to
# split lists of file paths, including version_locations and prepend_sys_path
# within configparser files such as alembic.ini.
#
# The default rendered in new alembic.ini files is "os", which uses os.pathsep
# to provide os-dependent path splitting.
#
# Note that in order to support legacy alembic.ini files, this default does NOT
# take place if path_separator is not present in alembic.ini.  If this
# option is omitted entirely, fallback logic is as follows:
#
# 1. Parsing of the version_locations option falls back to using the legacy
#    "version_path_separator" key, which if absent then falls back to the legacy
#    behavior of splitting on spaces and/or commas.
# 2. Parsing of the prepend_sys_path option falls back to the legacy
#    behavior of splitting on spaces, commas, or colons.
#
# Valid values for path_separator are:
#
# path_separator = :
# path_separator = ;
# path_separator = space
# path_separator = newline
#
# Use os.pathsep. Default configuration used for new projects.
path_separator = os

# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false

# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8

# database URL.  This is consumed by the user-maintained env.py script only.
# other means of configuring database URLs may be customized within the env.py
# file.
# See notes in "escaping characters in ini files" for guidelines on
# passwords
sqlalchemy.url = driver://user:pass@localhost/dbname

# [post_write_hooks]
# This section defines scripts or Python functions that are run
# on newly generated revision scripts.  See the documentation for further
# detail and examples

# format using "black" - use the console_scripts runner,
# against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME

# lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module
# hooks = ruff
# ruff.type = module
# ruff.module = ruff
# ruff.options = check --fix REVISION_SCRIPT_FILENAME

# Alternatively, use the exec runner to execute a binary found on your PATH
# hooks = ruff
# ruff.type = exec
# ruff.executable = ruff
# ruff.options = check --fix REVISION_SCRIPT_FILENAME

# Logging configuration.  This is also consumed by the user-maintained
# env.py script only.
[loggers]
keys = root,sqlalchemy,alembic

[handlers]
keys = console

[formatters]
keys = generic

[logger_root]
level = WARNING
handlers = console
qualname =

[logger_sqlalchemy]
level = WARNING
handlers =
qualname = sqlalchemy.engine

[logger_alembic]
level = INFO
handlers =
qualname = alembic

[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic

[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

Alembic 用 Python 的 configparser.ConfigParser 库读取 alembic.ini。%(here)s 是一个替换变量,值为 alembic.ini 本身所在位置的绝对路径。借助它,可以生成相对于配置文件位置的正确目录和文件路径。

此文件包含以下配置项:

  • [alembic]:Alembic 读取此节来确定配置。其核心实现不会直接读取文件中的其他区域,但可由用户定制的 env.py 读取附加指令(见下方说明)。对于 configparser 配置(不包括 pyproject.toml),可以用命令行 --name 标志自定义 alembic 节的名称;基本示例见从同一个 .ini 文件运行多个 Alembic 环境。
  • script_location:Alembic 环境的位置。通常是相对于 %(here)s(即配置文件本身位置)的文件系统路径;也可以是相对于当前目录的普通相对路径,或者绝对路径。

    这是 Alembic 在所有情况下唯一必需的配置键。alembic init alembic 在生成 .ini 文件时会自动将目录名 alembic 填入这里。也可使用 %(here)s/alembic。

    为了支持将应用打包成 .egg 文件,也可以指定包资源,由 resource_filename() 定位文件(0.2.2 新增)。任何包含冒号的非绝对 URI,在这里都会被解释为资源名,而不是普通文件名。

  • file_template:生成新迁移文件的命名方式。如希望在迁移文件名前加上日期和时间,以便按时间排序,可取消示例值的注释。默认值为 %%(rev)s_%%(slug)s。可用标记包括:
    • %%(rev)s:修订 ID。
    • %%(slug)s:从修订消息中取得并截断的字符串。
    • %%(epoch)s:依据创建日期产生的纪元时间戳,使用 Python 的 datetime.timestamp() 方法计算。
    • %%(year)d、%%(month).2d、%%(day).2d、%%(hour).2d、%%(minute).2d、%%(second).2d:创建日期各部分。默认使用 datetime.datetime.now(),除非同时使用 timezone 配置。

    file_template 还可以包含目录分隔符,从而把迁移文件放在子目录中。使用这种目录路径时,必须将 recursive_version_locations 设为 true。例如:

    file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s
    recursive_version_locations = true
    

    它会产生按日期组织的迁移文件,例如 versions/2024/12/26_143022_abc123_add_user_table.py。

  • timezone:可选时区名称(例如 UTC、EST5EDT),用于迁移文件注释内及文件名中的时间戳。需要 Python ≥ 3.9,或安装 backports.zoneinfo 和 tzdata。指定后,创建日期不再来自 datetime.datetime.now(),而按以下方式生成:
    datetime.datetime.utcnow().replace(
      tzinfo=datetime.timezone.utc
    ).astimezone(ZoneInfo(<timezone>))
    
  • truncate_slug_length:slug 字段可包含的最大字符数,默认为40。
  • sqlalchemy.url:通过 SQLAlchemy 连接数据库的 URL。只有 env.py 调用它时才使用这个配置。在 generic 模板中,run_migrations_offline() 内的 config.get_main_option("sqlalchemy.url") 和 run_migrations_online() 内的 engine_from_config(prefix="sqlalchemy.") 会引用它。如果 URL 应从其他来源取得,例如环境变量或全局注册表,或者迁移环境需要多个数据库 URL,开发者应按需要修改 env.py 来获取 URL。
  • revision_environment:设为 true 时,生成新修订文件以及运行 alembic history 时,都会无条件运行迁移环境脚本 env.py。
  • sourceless:设为 true 时,versions 目录内只有 .pyc 或 .pyo 的文件也会作为版本脚本使用,支持没有源码的版本目录。默认 false 时只使用 .py 文件。
  • version_locations:可选的修订文件位置列表,允许修订同时位于多个目录。示例见使用多个基线。
  • path_separator:version_locations 与 prepend_sys_path 路径列表的分隔符。仅用于 configparser 配置;pyproject.toml 配置不需要它。见使用多个基线。
  • recursive_version_locations:设为 true 时,在每个 version_locations 目录中递归搜索修订文件。1.10 新增。
  • output_encoding:Alembic 将 script.py.mako 写入新迁移文件时使用的编码,默认为 utf-8。
  • [loggers]、[handlers]、[formatters]、[logger_*]、[handler_*]、[formatter_*]:这些节属于 Python 标准日志配置,其机制见配置文件格式。与数据库连接一样,这些指令由 env.py 中的 logging.config.fileConfig() 调用直接使用,你可以修改该调用。

如果只使用一个数据库和 generic 配置,开始时只需设置 SQLAlchemy URL:

sqlalchemy.url = postgresql://scott:tiger@localhost/test

ini 文件中的字符转义

如前所述,Alembic 用 Python 的 ConfigParser 解析 .ini 文件,并启用其插值功能,以支持 %(here)s 以及创建自定义 Config 对象时通过 Config.config_args 参数设置的其他标记。

因此,字符串中的百分号如果不是插值变量的一部分,就必须重复一次来转义。例如 Python 脚本中的配置值:

my_configuration_value = "some % string"

若要从 .ini 文件解析,应写为:

[alembic]

my_configuration_value = some %% string

示例 alembic.ini 的 file_template 等配置中就体现了这种转义:

# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s

这里真正传给 Alembic 文件生成系统的 file_template 是 %(year)d_%(month).2d_%(day).2d_%(hour).2d%(minute).2d-%(rev)s_%(slug)s。

在 SQLAlchemy URL 中,百分号用于转义 @、百分号自身等具有语法意义的字符。例如密码 "P@ssw%rd":

>>> my_actual_password = "P@ssw%rd"

按 SQLAlchemy 文档所述,URL 中的 @ 和百分号要使用 urllib.parse.quote_plus 转义:

>>> import urllib.parse
>>> sqlalchemy_quoted_password = urllib.parse.quote_plus(my_actual_password)
>>> sqlalchemy_quoted_password
'P%40ssw%25rd'

SQLAlchemy 自身将 URL 转成字符串时,也会使用这种 URL 转义:

>>> from sqlalchemy import URL
>>> URL.create(
...   "some_db", username="scott", password=my_actual_password, host="host"
... ).render_as_string(hide_password=False)
'some_db://scott:P%40ssw%25rd@host'

若要将上面转义后的密码字符串放入启用百分号插值的 ConfigParser 文件,还必须把 % 重复一次:

>>> sqlalchemy_quoted_password.replace("%", "%%")
'P%%40ssw%%25rd'

下面是一个完整程序:它依据给定的数据库连接信息组装 URL,显示正确的 configparser 形式,并演示如何用断言检查这些形式:

from sqlalchemy import URL, make_url

database_driver = input("database driver? ")
username = input("username? ")
password = input("password? ")
host = input("host? ")
port = input("port? ")
database = input("database? ")

sqlalchemy_url = URL.create(
    drivername=database_driver,
    username=username,
    password=password,
    host=host,
    port=int(port),
    database=database,
)

stringified_sqlalchemy_url = sqlalchemy_url.render_as_string(
    hide_password=False
)

# assert make_url round trip
assert make_url(stringified_sqlalchemy_url) == sqlalchemy_url

print(
    f"The correctly escaped string that can be passed "
    f"to SQLAlchemy make_url() and create_engine() is:"
    f"\n\n     {stringified_sqlalchemy_url!r}\n"
)

percent_replaced_url = stringified_sqlalchemy_url.replace("%", "%%")

# assert percent-interpolated plus make_url round trip
assert make_url(percent_replaced_url % {}) == sqlalchemy_url

print(
    f"The SQLAlchemy URL that can be placed in a ConfigParser "
    f"file such as alembic.ini is:\n\n      "
    f"sqlalchemy.url = {percent_replaced_url}\n"
)

该程序应能消除将 SQLAlchemy URL 写入 configparser 文件时的歧义:

$ python alembic_pw_script.py
database driver? postgresql+psycopg2
username? scott
password? P@ssw%rd
host? localhost
port? 5432
database? testdb
The correctly escaped string that can be passed to SQLAlchemy make_url() and create_engine() is:

    'postgresql+psycopg2://scott:P%40ssw%25rd@localhost:5432/testdb'

The SQLAlchemy URL that can be placed in a ConfigParser file such as alembic.ini is:

      sqlalchemy.url = postgresql+psycopg2://scott:P%%40ssw%%25rd@localhost:5432/testdb

使用 pyproject.toml 配置

alembic.ini 中有一部分选项专用于本地环境中 Python 代码的组织和生成。这些选项也可以放到应用的 pyproject.toml 中,以使用符合 PEP 621 的配置。

使用 pyproject.toml 并不排斥同时保留 alembic.ini;后者仍是数据库 URL、连接选项和日志等部署细节的默认配置位置。不过连接和日志配置只由用户管理的 env.py 使用,因此如果这些配置从应用的其他位置取得,迁移环境完全可以不要求 alembic.ini 存在。只有 pyproject.toml、没有 alembic.ini 时,Alembic 仍能成功运行。

要开始使用 pyproject 配置,最直接的方法是选择 pyproject 模板:

alembic init --template pyproject alembic

输出会说明,它正在向现有 pyproject 文件追加指令:

Creating directory /path/to/yourproject/alembic...done
Creating directory /path/to/yourproject/alembic/versions...done
Appending to /path/to/yourproject/pyproject.toml...done
Generating /path/to/yourproject/alembic.ini...done
Generating /path/to/yourproject/alembic/env.py...done
Generating /path/to/yourproject/alembic/README...done
Generating /path/to/yourproject/alembic/script.py.mako...done
Please edit configuration/connection/logging settings in
'/path/to/yourproject/pyproject.toml' and
'/path/to/yourproject/alembic.ini' before proceeding.

如果 pyproject.toml 不存在,Alembic 的模板运行器会创建它;如果存在且尚无 Alembic 指令,就向它追加这些指令。

默认生成的 pyproject.toml 配置节与 alembic.ini 基本相同,但它直接支持值列表,这意味着 prepend_sys_path 和 version_locations 可以写成列表。%(here)s 标记仍表示 pyproject.toml 的绝对路径:

[tool.alembic]
# path to migration scripts
script_location = "%(here)s/alembic"

# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# Or organize into date-based subdirectories (requires recursive_version_locations = true)
# file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s

# additional paths to be prepended to sys.path. defaults to the current working directory.
prepend_sys_path = [
    "."
]

# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library.
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
# string value is passed to ZoneInfo()
# leave blank for localtime
# timezone =

# max length of characters to apply to the
# "slug" field
# truncate_slug_length = 40

# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false

# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false

# version location specification; This defaults
# to <script_location>/versions.  When using multiple version
# directories, initial revisions must be specified with --version-path.
# version_locations = [
#    "%(here)s/alembic/versions",
#    "%(here)s/foo/bar"
# ]

# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false

# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = "utf-8"


# This section defines scripts or Python functions that are run
# on newly generated revision scripts.  See the documentation for further
# detail and examples
# [[tool.alembic.post_write_hooks]]
# format using "black" - use the console_scripts runner,
# against the "black" entrypoint
# name = "black"
# type = "console_scripts"
# entrypoint = "black"
# options = "-l 79 REVISION_SCRIPT_FILENAME"
#
# [[tool.alembic.post_write_hooks]]
# lint with attempts to fix using "ruff" - use the exec runner,
# execute a binary
# name = "ruff"
# type = "exec"
# executable = "%(here)s/.venv/bin/ruff"
# options = "check --fix REVISION_SCRIPT_FILENAME"

该模板的 alembic.ini 会被精简,只保留数据库配置与日志配置:

[alembic]

# database URL.  This is consumed by the user-maintained env.py script only.
# other means of configuring database URLs may be customized within the env.py
# file.
sqlalchemy.url = driver://user:pass@localhost/dbname

# Logging configuration.  This is also consumed by the user-maintained
# env.py script only.
[loggers]
keys = root,sqlalchemy,alembic

[handlers]
keys = console

[formatters]
keys = generic

[logger_root]
level = WARNING
handlers = console
qualname =

[logger_sqlalchemy]
level = WARNING
handlers =
qualname = sqlalchemy.engine

[logger_alembic]
level = INFO
handlers =
qualname = alembic

[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic

[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

如果配置 env.py,让它从 alembic.ini 以外的位置获取数据库连接与日志配置,这个文件可以完全省略。

创建迁移脚本

环境就绪后,可以使用 alembic revision 创建新修订:

$ alembic revision -m "create account table"
Generating /path/to/yourproject/alembic/versions/1975ea83b712_create_accoun
t_table.py...done

新文件 1975ea83b712_create_account_table.py 已生成,内容如下:

"""create account table

Revision ID: 1975ea83b712
Revises:
Create Date: 2011-11-08 11:40:27.089406

"""

# revision identifiers, used by Alembic.
revision = '1975ea83b712'
down_revision = None
branch_labels = None

from alembic import op
import sqlalchemy as sa

def upgrade():
    pass

def downgrade():
    pass

文件包含头部信息、当前修订和“降级”修订的标识符、基本 Alembic 指令的导入,以及空的 upgrade() 和 downgrade() 函数。我们要在两个函数中填入指令,对数据库应用一组更改。通常必须提供 upgrade();只有需要回退到旧修订时才需要 downgrade(),不过提供降级功能通常是个好主意。

还应注意 down_revision 变量。Alembic 用它确定迁移的正确顺序。创建下一个修订时,新文件的 down_revision 会指向当前这个修订:

# revision identifiers, used by Alembic.
revision = 'ae1027a6acf'
down_revision = '1975ea83b712'

Alembic 每次操作 versions/ 目录时都会读取所有文件,并依据 down_revision 标识符之间的连接建立列表;down_revision 为 None 表示第一个文件。理论上,如果环境中有数千个迁移,这可能增加启动延迟,但实践中项目通常应适时裁剪旧迁移(见从头构建最新数据库,了解如何裁剪的同时保留完整构建当前数据库的能力)。

然后向脚本添加指令。假设需要创建一个新的 account 表:

def upgrade():
    op.create_table(
        'account',
        sa.Column('id', sa.Integer, primary_key=True),
        sa.Column('name', sa.String(50), nullable=False),
        sa.Column('description', sa.Unicode(200)),
    )

def downgrade():
    op.drop_table('account')

create_table() 与 drop_table() 都是 Alembic 指令。Alembic 用这类指令提供所有基本数据库迁移操作,设计尽可能简洁;多数指令不依赖已有的表元数据。它们利用全局“上下文”获取数据库连接(如果存在连接;迁移也能将 SQL/DDL 指令输出到文件),然后执行命令。与其他内容一样,这个全局上下文在 env.py 中配置。

所有 Alembic 指令的概览见操作参考。

执行第一次迁移

现在来执行迁移。假定数据库完全为空,还没有版本。alembic upgrade 会从当前数据库修订(本例为 None)逐步升级到指定目标修订。可以指定 1975ea83b712,但多数情况下用“最新修订”,即 head,更方便:

$ alembic upgrade head
INFO  [alembic.context] Context class PostgresqlContext.
INFO  [alembic.context] Will assume transactional DDL.
INFO  [alembic.context] Running upgrade None -> 1975ea83b712

成功了!屏幕上的信息来自 alembic.ini 中的日志配置:它将 alembic 日志流输出到控制台,具体是标准错误。

Alembic 首先检查数据库是否有 alembic_version 表,没有就创建它;然后从该表读取当前版本(如果有),计算当前版本到目标版本的路径。本例的目标是 head,也就是 1975ea83b712。随后调用沿途每个文件的 upgrade(),到达目标修订。

执行第二次迁移

再做一次迁移,以便有更多内容可供演示。再次创建修订文件:

$ alembic revision -m "Add a column"
Generating /path/to/yourapp/alembic/versions/ae1027a6acf_add_a_column.py...
done

编辑这个文件,为 account 表添加一列:

"""Add a column

Revision ID: ae1027a6acf
Revises: 1975ea83b712
Create Date: 2011-11-08 12:37:36.714947

"""

# revision identifiers, used by Alembic.
revision = 'ae1027a6acf'
down_revision = '1975ea83b712'

from alembic import op
import sqlalchemy as sa

def upgrade():
    op.add_column('account', sa.Column('last_transaction_date', sa.DateTime))

def downgrade():
    op.drop_column('account', 'last_transaction_date')

再次升级到 head:

$ alembic upgrade head
INFO  [alembic.context] Context class PostgresqlContext.
INFO  [alembic.context] Will assume transactional DDL.
INFO  [alembic.context] Running upgrade 1975ea83b712 -> ae1027a6acf

数据库中现在已添加 last_transaction_date 列。

部分修订标识符

任何需要显式指定修订编号的地方,都可以使用部分编号。只要能唯一识别该版本,就可以在接受版本编号的任何命令和位置使用:

$ alembic upgrade ae1

这里用 ae1 指代 ae1027a6acf。如果有多个版本以该前缀开头,Alembic 会停止并告知你。

相对迁移标识符

Alembic 也支持相对升级和降级。若从当前修订向前移动两个版本,可以提供十进制形式的 +N:

$ alembic upgrade +2

降级接受负值:

$ alembic downgrade -1

相对标识符也可以以某个指定修订为基准。例如先定位到 ae1027a6acf,再向前升级两步:

$ alembic upgrade ae10+2

获取信息

有了若干修订后,就可以查看状态信息。首先查看当前修订:

$ alembic current
INFO  [alembic.context] Context class PostgresqlContext.
INFO  [alembic.context] Will assume transactional DDL.
Current revision for postgresql://scott:XXXXX@localhost/test: 1975ea83b712 -> ae1027a6acf (head), Add a column

只有数据库的修订标识符与最新修订匹配时,才显示 head。

还可用 alembic history 查看历史。--verbose 选项会显示每个修订的完整信息;包括 history、current、heads 和 branches 在内的多个命令都接受它:

$ alembic history --verbose

Rev: ae1027a6acf (head)
Parent: 1975ea83b712
Path: /path/to/yourproject/alembic/versions/ae1027a6acf_add_a_column.py

    add a column

    Revision ID: ae1027a6acf
    Revises: 1975ea83b712
    Create Date: 2014-11-20 13:02:54.849677

Rev: 1975ea83b712
Parent: <base>
Path: /path/to/yourproject/alembic/versions/1975ea83b712_add_account_table.py

    create account table

    Revision ID: 1975ea83b712
    Revises:
    Create Date: 2014-11-20 13:02:46.257104

查看历史范围

使用 alembic history 的 -r 选项,可以查看不同的历史片段。其参数形式是 [start]:[end]。两端都可以是修订编号,或 head、heads、base 等符号,也可用 current 指定当前修订;[start] 支持负的相对范围,[end] 支持正的相对范围:

$ alembic history -r1975ea:ae1027

以下相对范围从三个修订之前开始,直到当前迁移。它会对数据库调用迁移环境,以获得当前迁移:

$ alembic history -r-3:current

查看从 1975 开始到最新修订的所有版本:

$ alembic history -r1975ea:

降级

可以调用 alembic downgrade 回到起点,在 Alembic 中称为 base,以演示如何降级到没有任何迁移的状态:

$ alembic downgrade base
INFO  [alembic.context] Context class PostgresqlContext.
INFO  [alembic.context] Will assume transactional DDL.
INFO  [alembic.context] Running downgrade ae1027a6acf -> 1975ea83b712
INFO  [alembic.context] Running downgrade 1975ea83b712 -> None

回到空状态之后,再升级:

$ alembic upgrade head
INFO  [alembic.context] Context class PostgresqlContext.
INFO  [alembic.context] Will assume transactional DDL.
INFO  [alembic.context] Running upgrade None -> 1975ea83b712
INFO  [alembic.context] Running upgrade 1975ea83b712 -> ae1027a6acf

下一步

绝大多数 Alembic 环境都会大量使用“自动生成(autogenerate)”功能。请继续阅读下一节自动生成迁移。

原文:Tutorial — Alembic 官方文档。本版本完整翻译正文并调整文章排版,示例与历史输出保留原样。文档中的新增/变更版本说明依原文保留。

版权与 MIT 许可
Copyright 2009-2026 Michael Bayer.

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

官方许可文件

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容