教程#

Alembic 提供了使用 SQLAlchemy 作为底层引擎为关系数据库创建、管理和调用变更管理脚本的功能。本教程将全面介绍此工具的理论和用法。

首先,请确保已按照 安装 中所述安装 Alembic。如链接文档中所述,通常最好将 Alembic 安装在与目标项目相同的模块/Python 路径中,通常使用 Python 虚拟环境,以便在运行 alembic 命令时,由 alembic 调用的 Python 脚本(即项目的 env.py 脚本)可以访问应用程序的模型。在所有情况下,这并非严格必要,但在绝大多数情况下通常是首选。

以下教程假定 alembic 命令行实用程序存在于本地路径中,并且在调用时,可以访问与目标项目相同的 Python 模块环境。

迁移环境#

使用 Alembic 从创建迁移环境开始。这是一个特定于某个应用程序的脚本目录。迁移环境只创建一次,然后与应用程序的源代码本身一起维护。该环境使用 Alembic 的 init 命令创建,然后可以根据应用程序的特定需求进行自定义。

此环境的结构(包括一些生成的迁移脚本)如下所示

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

该目录包括以下目录/文件

  • 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 创建一个环境。这将使用“通用”模板创建一个环境

$ 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.

Alembic 还包括其他环境模板。可以使用 list_templates 命令列出这些模板

$ alembic list_templates
Available templates:

generic - Generic single-database configuration.
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

1.8 版中已更改:已删除“pylons”环境模板。

编辑 .ini 文件#

Alembic 将文件 alembic.ini 放置在当前目录中。这是 alembic 脚本在调用时查找的文件。此文件可以存在于不同的目录中,其位置由 alembic 运行器的 --config 选项或 ALEMBIC_CONFIG 环境变量指定(前者优先)。

使用“通用”配置生成的文件如下所示

# A generic, single database configuration.

[alembic]
# path to migration scripts
script_location = 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

# 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.
# 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 path separator used here should be the separator specified by "version_path_separator" below.
# version_locations = %(here)s/bar:%(here)s/bat:${script_location}/versions

# version path separator; As mentioned above, this is the character used to split
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
# Valid values for version_path_separator are:
#
# version_path_separator = :
# version_path_separator = ;
# version_path_separator = space
version_path_separator = os  # Use os.pathsep. Default configuration used for new projects.

# 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

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 exec runner, execute a binary
# hooks = ruff
# ruff.type = exec
# ruff.executable = %(here)s/.venv/bin/ruff
# ruff.options = --fix REVISION_SCRIPT_FILENAME

# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic

[handlers]
keys = console

[formatters]
keys = generic

[logger_root]
level = WARN
handlers = console
qualname =

[logger_sqlalchemy]
level = WARN
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

使用 Python 的 ConfigParser.SafeConfigParser 对象读取文件。提供 %(here)s 变量作为替换变量,可用于生成目录和文件的绝对路径名,如我们在上面使用 Alembic 脚本位置的路径所做的那样。

此文件包含以下功能

  • [alembic] - 这是 Alembic 读取以确定配置的部分。Alembic 的核心实现不会直接读取文件的任何其他区域,不包括可能从终端用户可自定义的 env.py 文件中使用的其他指令(请参见下面的注释)。可以使用 --name 命令行标志自定义“alembic”名称;有关此内容的基本示例,请参见 从一个 .ini 文件运行多个 Alembic 环境

    注意

    Alembic 环境模板中包含的默认 env.py 文件还将从日志记录部分 [logging][handlers] 等读取。如果正在使用的配置文件不包含日志记录指令,请从生成的 env.py 文件中移除 fileConfig() 指令,以防止它尝试配置日志记录。

  • script_location - 这是 Alembic 环境的位置。它通常指定为文件系统位置,相对或绝对。如果位置是相对路径,则解释为相对于当前目录。

    这是 Alembic 在所有情况下唯一需要的键。命令 alembic init alembic 生成的 .ini 文件自动将目录名称 alembic 放置在此处。特殊变量 %(here)s 也可以使用,如 %(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 配置选项。

    1.8 版中新增:添加了“epoch”

  • timezone - 可选时区名称(例如 UTCEST5EDT 等),它将应用于迁移文件注释中以及文件名中呈现的时间戳。此选项需要 Python>=3.9 或安装 backports.zoneinfo 库。如果指定了 timezone,则创建日期对象不再从 datetime.datetime.now() 派生,而是生成如下

    datetime.datetime.utcnow().replace(
      tzinfo=datetime.timezone.utc
    ).astimezone(ZoneInfo(<timezone>))
    

    1.13.0 版中更改:迁移中现在使用 Python 标准库 zoneinfo 进行时区渲染;以前使用 python-dateutil

  • truncate_slug_length - 默认为 40,是“slug”字段中包含的最大字符数。

  • sqlalchemy.url - 通过 SQLAlchemy 连接到数据库的 URL。仅当 env.py 文件调用此配置值时,才使用此配置值;在“通用”模板中,run_migrations_offline() 函数中对 config.get_main_option("sqlalchemy.url") 的调用和 run_migrations_online() 函数中对 engine_from_config(prefix="sqlalchemy.") 的调用是引用此键的位置。如果 SQLAlchemy URL 应来自其他来源(例如来自环境变量或全局注册表),或者如果迁移环境使用多个数据库 URL,则建议开发人员更改 env.py 文件以使用任何适当的方法来获取数据库 URL 或 URL。

  • revision_environment - 当此标志设置为值“true”时,将指示迁移环境脚本 env.py 在生成新修订文件以及运行 alembic history 命令时应无条件运行。

  • sourceless - 当设置为“true”时,仅作为 versions 目录中的 .pyc 或 .pyo 文件存在的修订文件将用作版本,从而允许“无源”版本控制文件夹。当保留为默认的“false”时,仅 .py 文件被用作版本文件。

  • version_locations - 可选的修订文件位置列表,允许修订同时存在于多个目录中。有关示例,请参阅 使用多个基准

  • version_path_separator - version_locations 路径的分隔符。如果使用多个 version_locations,则应定义它。有关示例,请参阅 使用多个基准

  • 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() 调用的结果,你可以自由修改它。

对于仅使用单个数据库和通用配置,设置 SQLAlchemy URL 是唯一需要做的

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

创建迁移脚本#

有了这个环境,我们可以使用 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() 函数。通常,需要 upgrade(),而仅当需要降级功能时才需要 downgrade(),尽管这可能是个好主意。

另一个需要注意的是 down_revision 变量。这是 Alembic 了解应用迁移的正确顺序的方式。当我们创建下一个修订时,新文件的 down_revision 标识符将指向此文件

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

每次 Alembic 对 versions/ 目录运行操作时,它都会读取其中的所有文件,并根据 down_revision 标识符如何链接在一起来编写一个列表,其中 down_revisionNone 的表示第一个文件。从理论上讲,如果一个迁移环境有数千个迁移,这可能会开始给启动增加一些延迟,但实际上一个项目可能应该修剪旧迁移(请参阅部分 从头开始构建一个最新的数据库,了解如何执行此操作,同时保持完全构建当前数据库的能力)。

然后,我们可以向脚本添加一些指令,假设添加一个新表 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 将停止并通知您。

相对迁移标识符#

还支持相对升级/降级。要从当前版本移动两个版本,可以提供十进制值“+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 选项(由多个命令接受,包括 historycurrentheadsbranches)将向我们显示有关每个修订的完整信息

$ 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

查看历史记录范围#

使用 -r 选项到 alembic history,我们还可以查看历史记录的各个部分。 -r 参数接受一个参数 [start]:[end],其中任何一个都可以是修订号、符号(如 headheadsbasecurrent)以指定当前修订,以及 [start] 的负相对范围和 [end] 的正相对范围

$ alembic history -r1975ea:ae1027

从三轮前开始到当前迁移的相对范围,它将调用针对数据库的迁移环境以获取当前迁移

$ alembic history -r-3:current

注意

如上所示,要使用以负数(即破折号)开头的范围,由于 argparse 中的 bug,必须使用语法 -r-<base>:<head>,没有任何空格,如上所示

$ alembic history -r-3:current

或者如果使用 --rev-range,则必须使用等号

$ alembic history --rev-range=-3:current

如果参数名称后面有空格,则使用引号或转义符号不起作用。

查看从 1975 年到 head 的所有修订

$ 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 环境大量使用“自动生成”功能。继续进入下一部分,自动生成迁移