自动生成迁移#

Alembic 可以查看数据库的状态,并与应用程序中的表元数据进行比较,根据比较生成“明显的”迁移。这是通过对 alembic revision 命令使用 --autogenerate 选项来实现的,该选项将所谓的候选迁移放入我们的新迁移文件中。我们根据需要手动查看并修改这些迁移,然后正常进行。

要使用自动生成,我们首先需要修改我们的 env.py,以便它可以访问包含目标的表元数据对象。假设我们的应用程序在 myapp.mymodel 中有一个 声明基。此基包含一个 MetaData 对象,其中包含定义我们数据库的 Table 对象。我们确保在 env.py 中加载此内容,然后通过 target_metadata 参数将其传递给 EnvironmentContext.configure()。通用模板中使用的 env.py 示例脚本在顶部附近已经有一个变量声明,以便我们方便地将 None 替换为我们的 MetaData。从

# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = None

我们更改为

from myapp.mymodel import Base
target_metadata = Base.metadata

注意

上面的示例指的是通用 alembic env.py 模板,例如在调用 alembic init 时默认创建的模板,而不是 multidb 等特殊用途模板。请直接查阅 env.py 脚本中的源代码和注释,以获取有关在何处以及如何建立自动生成元数据的具体指导。

如果我们在脚本中稍后查看,在 run_migrations_online() 中,我们可以看到传递给 EnvironmentContext.configure() 的指令

def run_migrations_online():
    engine = engine_from_config(
                config.get_section(config.config_ini_section), prefix='sqlalchemy.')

    with engine.connect() as connection:
        context.configure(
                    connection=connection,
                    target_metadata=target_metadata
                    )

        with context.begin_transaction():
            context.run_migrations()

然后,我们可以结合 --autogenerate 选项使用 alembic revision 命令。假设我们的 MetaData 包含了 account 表的定义,但数据库没有。我们会得到类似这样的输出

$ alembic revision --autogenerate -m "Added account table"
INFO [alembic.context] Detected added table 'account'
Generating /path/to/foo/alembic/versions/27c6a30d7c24.py...done

然后我们可以查看我们的文件 27c6a30d7c24.py 并看到一个基本的迁移已经存在

"""empty message

Revision ID: 27c6a30d7c24
Revises: None
Create Date: 2011-11-08 11:40:27.089406

"""

# revision identifiers, used by Alembic.
revision = '27c6a30d7c24'
down_revision = None

from alembic import op
import sqlalchemy as sa

def upgrade():
    ### commands auto generated by Alembic - please adjust! ###
    op.create_table(
    'account',
    sa.Column('id', sa.Integer()),
    sa.Column('name', sa.String(length=50), nullable=False),
    sa.Column('description', sa.VARCHAR(200)),
    sa.Column('last_transaction_date', sa.DateTime()),
    sa.PrimaryKeyConstraint('id')
    )
    ### end Alembic commands ###

def downgrade():
    ### commands auto generated by Alembic - please adjust! ###
    op.drop_table("account")
    ### end Alembic commands ###

当然,迁移实际上还没有运行。我们通过通常的 upgrade 命令来执行此操作。我们还应该进入我们的迁移文件并根据需要对其进行更改,包括对指令的调整以及添加这些指令可能依赖的其他指令 - 具体来说是在创建/更改/删除之间的数据更改。

自动生成检测会检测什么(以及它不会检测什么?#

用户使用 Alembic 时遇到的绝大多数问题都集中在自动生成可以和不能可靠检测的更改类型,以及它如何为检测到的内容呈现 Python 代码。需要注意的是,自动生成并不旨在完美无缺始终有必要手动检查和更正自动生成生成的候选迁移。随着版本的不断发布,此功能变得越来越全面且无错误,但我们应该注意当前的限制。

自动生成将检测

  • 表添加、删除。

  • 列添加、删除。

  • 列上可空状态的更改。

  • 索引和显式命名的唯一约束中的基本更改

  • 外键约束中的基本更改

自动生成可以选择性检测

  • 列类型的更改。除非将参数 EnvironmentContext.configure.compare_type 设置为 False,否则将默认执行此操作。默认实现将可靠地检测到重大更改,例如 NumericString 之间的更改,以及适应 SQLAlchemy 的“通用”类型(例如 Boolean)生成的类型。还将比较两种类型之间共享的参数,例如长度和精度值。如果元数据类型或数据库类型具有超出另一种类型的其他参数,则比较这些参数,例如,如果一种数字类型具有“刻度”而另一种类型没有,这将被视为后端数据库不支持该值,或报告元数据未指定的一个默认值。

    类型比较逻辑也是完全可扩展的;有关详细信息,请参阅 比较类型

  • 服务器默认值的更改。如果您将 EnvironmentContext.configure.compare_server_default 参数设置为 True 或自定义可调用函数,则将执行此操作。此功能适用于简单的情况,但不能始终产生准确的结果。Postgresql 后端实际上会针对数据库调用“检测到的”和“元数据”值以确定等效性。此功能默认关闭,以便首先在目标模式上对其进行测试。与类型比较类似,它还可以通过传递一个可调用函数进行自定义;有关详细信息,请参阅函数文档。

自动生成无法检测

  • 表名称的更改。这些将作为两个不同表的添加/删除出现,并且应该手动编辑成名称更改。

  • 列名称的更改。与表名称更改类似,这些更改被检测为列添加/删除对,这与名称更改完全不同。

  • 匿名约束。为您的约束命名,例如 UniqueConstraint('col1', 'col2', name="my_name")。请参阅章节 约束命名的重要性,了解如何为约束配置自动命名方案的背景信息。

  • 当在不支持 ENUM 的后端上生成时,诸如 Enum 等特殊 SQLAlchemy 类型 - 这是因为此类类型在不支持数据库中的表示形式,即 CHAR+CHECK 约束,可以是任何类型的 CHAR+CHECK。对于 SQLAlchemy 来说,确定这实际上是一个 ENUM 仅仅是一个猜测,这通常是一个坏主意。要在此处实现您自己的“猜测”函数,请使用 sqlalchemy.events.DDLEvents.column_reflect() 事件来检测何时反映 CHAR(或目标类型是什么),如果已知这是该类型的意图,则将其更改为 ENUM(或所需的任何类型)。sqlalchemy.events.DDLEvents.after_parent_attach() 可以在自动生成过程中使用,以拦截和取消附加不需要的 CHECK 约束。

自动生成目前不能,但最终将检测到

  • 某些独立的约束添加和删除可能不受支持,包括 PRIMARY KEY、EXCLUDE、CHECK;这些不一定在自动生成检测系统中实现,并且也不一定受支持的 SQLAlchemy 方言支持。

  • 序列添加、删除 - 尚未实现。

值得注意的第三方库,它们扩展了内置的 Alembic 自动生成功能#

  • alembic-utils 一个库,它添加了对 PostgreSQL 函数、视图、触发器等的自动生成支持。

  • alembic-postgresql-enum 一个库,它添加了对在 PostgreSQL 中创建、更改和删除 Enum 的自动生成支持。

自动生成多个元数据集合#

如果应用程序涉及多个 MetaData 集合,则 target_metadata 集合也可以定义为一个序列

from myapp.mymodel1 import Model1Base
from myapp.mymodel2 import Model2Base
target_metadata = [Model1Base.metadata, Model2Base.metadata]

在自动生成过程中,将按顺序查阅 MetaData 集合的序列。请注意,每个 MetaData 都必须包含唯一的表键(例如,“键”是表名和模式的组合);如果两个 MetaData 对象包含模式/名称组合相同的表,则会引发错误。

控制要自动生成的内容#

自动生成过程会扫描当前使用的数据库连接所引用的数据库中的所有表对象。

在目标数据库连接中扫描的对象列表包括

另请参见

指定模式名称 - 深入介绍 SQLAlchemy 如何解释模式名称

远程模式表内省和 PostgreSQL search_path - 针对 PostgreSQL 数据库的重要注意事项

从自动生成过程中省略模式名称#

由于上述数据库对象集通常要与单个 MetaData 对象的内容进行比较,尤其是在 EnvironmentContext.configure.include_schemas 标志启用时,迫切需要过滤掉不需要的“模式”,对于某些数据库后端而言,这可能是所有存在的数据库的列表。最好使用 EnvironmentContext.configure.include_name 钩子执行此过滤,该钩子提供一个可返回布尔值 true/false 的可调用对象,指示是否应包括特定模式名称

def include_name(name, type_, parent_names):
    if type_ == "schema":
        # note this will not include the default schema
        return name in ["schema_one", "schema_two"]
    else:
        return True

context.configure(
    # ...
    include_schemas = True,
    include_name = include_name
)

在上面,当首次检索架构名称列表时,名称将通过上面的 include_name 函数进行筛选,以便仅考虑名为 "schema_one""schema_two" 的架构由自动生成过程考虑。

为了包括默认架构,即在没有指定任何显式架构的情况下由数据库连接引用的架构,传递给挂钩的名称是 None。要更改我们的上述示例以还包括默认架构,我们也将其与 None 进行比较

def include_name(name, type_, parent_names):
    if type_ == "schema":
        # this **will* include the default schema
        return name in [None, "schema_one", "schema_two"]
    else:
        return True

context.configure(
    # ...
    include_schemas = True,
    include_name = include_name
)

从自动生成过程中省略表名#

最适合限制目标数据库中要考虑的表名的钩子是 EnvironmentContext.configure.include_name。如果目标数据库有许多不属于 MetaData 的表,自动生成过程通常会假设这些表是数据库中要删除的无关表,并且会为每个表生成 Operations.drop_table() 操作。为了防止这种情况,可以使用 EnvironmentContext.configure.include_name 钩子在 tables 集合中搜索每个名称 MetaData 对象并确保未出现的名称不包含在内

target_metadata = MyModel.metadata

def include_name(name, type_, parent_names):
    if type_ == "table":
        return name in target_metadata.tables
    else:
        return True

context.configure(
    # ...
    target_metadata = target_metadata,
    include_name = include_name,
    include_schemas = False
)

上面的示例仅限于默认架构中存在的表名。为了在 MetaData 集合中搜索架构限定的表名,非默认架构中存在的表将以 <schemaname>.<tablename> 形式的名称出现。 EnvironmentContext.configure.include_name 钩子将在 parent_names 字典中以每个表名为基础提供此架构名称,使用 "schema_name" 键引用当前正在考虑的架构的名称,或 None(如果架构是数据库连接的默认架构)

# example fragment

if parent_names["schema_name"] is None:
    return name in target_metadata.tables
else:
    # build out schema-qualified name explicitly...
    return (
        "%s.%s" % (parent_names["schema_name"], name) in
        target_metadata.tables
    )

但是更简单的是, parent_names 字典还将包含在 "schema_qualified_table_name" 键下构造的点连接名称,该名称也适用于默认架构中的表,并且省略了点。因此,省略支持架构的表的完整示例可能如下所示

target_metadata = MyModel.metadata

def include_name(name, type_, parent_names):
    if type_ == "schema":
        return name in [None, "schema_one", "schema_two"]
    elif type_ == "table":
        # use schema_qualified_table_name directly
        return (
            parent_names["schema_qualified_table_name"] in
            target_metadata.tables
        )
    else:
        return True

context.configure(
    # ...
    target_metadata = target_metadata,
    include_name = include_name,
    include_schemas = True
)

当正在考虑的名称是特定表的列或约束对象本地名称时,parent_names 字典还将包含键 "table_name"

EnvironmentContext.configure.include_name 钩子仅指反射的对象,而不是位于目标 MetaData 集合中的对象。对于同时包含 MetaData 和反射对象的更细粒度的规则,下一节中讨论的 EnvironmentContext.configure.include_object 钩子更合适。

基于对象省略#

EnvironmentContext.configure.include_object 钩子提供基于 Table 对象(以及其中的元素)的反映对象级别包含/排除规则。此钩子可用于限制来自本地 MetaData 集合以及目标数据库的对象。限制在于,当它报告数据库中的对象时,它将完全反映该对象,如果将省略大量对象,这可能会很昂贵。以下示例引用了一条细粒度规则,该规则将跳过对 Column 对象的更改,这些对象在 info 字典中放置了用户定义的标志 skip_autogenerate

def include_object(object, name, type_, reflected, compare_to):
    if (type_ == "column" and
        not reflected and
        object.info.get("skip_autogenerate", False)):
        return False
    else:
        return True

context.configure(
    # ...
    include_object = include_object
)

比较和呈现类型#

autogenerate 在迁移脚本中比较和呈现基于 Python 的类型对象的的行为领域提出了一个挑战,即脚本中要呈现的类型非常广泛,包括 SQLAlchemy 的一部分以及用户定义的类型。提供了一些选项来帮助完成此任务。

控制模块前缀#

呈现类型时,它们会使用模块前缀生成,以便根据相对较少的导入使用它们。前缀是什么的规则基于数据类型的种类以及配置设置。例如,当 Alembic 呈现 SQLAlchemy 类型时,它默认情况下会使用前缀 sa.

Column("my_column", sa.Integer())

可以通过更改 EnvironmentContext.configure.sqlalchemy_module_prefix 的值来控制 sa. 前缀的使用

def run_migrations_online():
    # ...

    context.configure(
                connection=connection,
                target_metadata=target_metadata,
                sqlalchemy_module_prefix="sqla.",
                # ...
                )

    # ...

无论哪种情况,sa. 前缀或任何所需的前缀也应包含在 script.py.mako 的导入部分中;它还默认为 import sqlalchemy as sa

对于用户定义的类型,即 sqlalchemy. 模块命名空间中不存在的任何自定义类型,默认情况下,Alembic 将使用自定义类型的 __module__ 值

Column("my_column", myapp.models.utils.types.MyCustomType())

上述类型的导入必须再次在迁移中显示,可以手动添加,也可以将其添加到 script.py.mako

上述自定义类型具有基于直接使用 __module__ 的冗长且繁琐的名称,这也意味着为了适应大量类型,需要大量导入。因此,建议在迁移脚本中使用的用户定义类型可从单个模块中获取。假设我们称之为 myapp.migration_types

# myapp/migration_types.py

from myapp.models.utils.types import MyCustomType

我们首先可以为 migration_types 添加一个导入到我们的 script.py.mako

from alembic import op
import sqlalchemy as sa
import myapp.migration_types
${imports if imports else ""}

然后,我们通过使用 EnvironmentContext.configure.user_module_prefix 选项提供一个固定前缀,覆盖 Alembic 对 __module__ 的使用

def run_migrations_online():
    # ...

    context.configure(
                connection=connection,
                target_metadata=target_metadata,
                user_module_prefix="myapp.migration_types.",
                # ...
                )

    # ...

在上面,我们现在将获得一个类似的迁移

Column("my_column", myapp.migration_types.MyCustomType())

现在,当我们不可避免地重构我们的应用程序以将 MyCustomType 移动到其他地方时,我们只需要修改 myapp.migration_types 模块,而不是在我们的迁移脚本中搜索并替换所有实例。

影响类型的渲染本身#

Alembic 用于将 SQLAlchemy 和用户定义的类型构造生成为 Python 代码的方法是普通的旧 __repr__()。SQLAlchemy 的内置类型在大多数情况下都有一个 __repr__(),它忠实地呈现一个与 Python 兼容的构造函数调用,但有一些例外,特别是在构造函数接受与 __repr__() 不兼容的参数(例如,一个 pickling 函数)的情况下。

在构建将呈现到迁移脚本中的自定义类型时,通常需要明确地为该类型提供一个 __repr__(),该 __repr__() 将忠实地重现该类型的构造函数。这与 EnvironmentContext.configure.user_module_prefix 相结合,通常就足够了。但是,如果需要其他行为,则更全面的挂钩是 EnvironmentContext.configure.render_item 选项。此挂钩允许我们在 env.py 中提供一个可调用函数,该函数将完全接管类型的呈现方式,包括其模块前缀

def render_item(type_, obj, autogen_context):
    """Apply custom rendering for selected items."""

    if type_ == 'type' and isinstance(obj, MySpecialType):
        return "mypackage.%r" % obj

    # default rendering for other objects
    return False

def run_migrations_online():
    # ...

    context.configure(
                connection=connection,
                target_metadata=target_metadata,
                render_item=render_item,
                # ...
                )

    # ...

在上面的示例中,我们将确保我们的 MySpecialType 包含一个适当的 __repr__() 方法,当我们针对 "%r" 调用它时,将调用该方法。

我们用于 EnvironmentContext.configure.render_item 的可调用对象还可以向我们的迁移脚本中添加导入。传入的 AutogenContext 包含一个名为 AutogenContext.imports 的数据成员,它是一个 Python set(),我们可以为其添加新的导入。例如,如果 MySpecialType 在名为 mymodel.types 的模块中,我们可以在遇到该类型时为其添加导入

def render_item(type_, obj, autogen_context):
    """Apply custom rendering for selected items."""

    if type_ == 'type' and isinstance(obj, MySpecialType):
        # add import for this type
        autogen_context.imports.add("from mymodel import types")
        return "types.%r" % obj

    # default rendering for other objects
    return False

完成的迁移脚本将在使用 ${imports} 表达式的位置包含我们的导入,生成类似于以下内容的输出

from alembic import op
import sqlalchemy as sa
from mymodel import types

def upgrade():
    op.add_column('sometable', Column('mycolumn', types.MySpecialType()))

比较类型#

默认类型比较逻辑适用于 SQLAlchemy 内置类型以及基本用户定义类型。此逻辑默认启用。可以通过将 EnvironmentContext.configure.compare_type 设置为 False 来禁用它

context.configure(
    # ...
    compare_type = False
)

在版本 1.12.0 中更改:EnvironmentContext.configure.compare_type 的默认值已更改为 True

注意

默认类型比较逻辑(这是最终用户可扩展的)当前(截至 Alembic 版本 1.4.0)通过比较列的生成 SQL 来工作。它通过两个步骤来执行此操作-

  • 首先,它会比较每列的外层类型,例如 VARCHARTEXT。方言实现可以有被视为等效的同义词,这是因为某些数据库通过将类型转换为另一种类型来支持类型。例如,NUMERIC 和 DECIMAL 在所有后端都被视为等效,而在 Oracle 后端,其他同义词 BIGINT、INTEGER、NUMBER、SMALLINT 被添加到此等效列表中

  • 接下来,比较类型中的参数,例如字符串的长度、数字的精度值、枚举中的元素。如果两列都有参数且它们不同,则将检测到更改。如果一列仅设置为默认值,而另一列有参数,则 Alembic 将尝试比较它们。其原理是,很难检测到数据库后端将什么设置为默认值,而不会产生误报。

或者,EnvironmentContext.configure.compare_type 参数接受一个可调用函数,该函数可用于实现自定义类型比较逻辑,例如在使用特殊用户定义类型的情况下

def my_compare_type(context, inspected_column,
            metadata_column, inspected_type, metadata_type):
    # return False if the metadata_type is the same as the inspected_type
    # or None to allow the default implementation to compare these
    # types. a return value of True means the two types do not
    # match and should result in a type change operation.
    return None

context.configure(
    # ...
    compare_type = my_compare_type
)

上面,inspected_columnsqlalchemy.schema.Column,由 sqlalchemy.engine.reflection.Inspector.reflect_table() 返回,而 metadata_column 是来自本地模型环境的 sqlalchemy.schema.Column。返回值 None 表示默认类型比较将继续进行。

将后处理和 Python 代码格式化程序应用于生成的修订#

alembic revision 命令生成的修订脚本可以选择通过一系列后处理函数进行管道处理,这些函数可能会分析或重写 Alembic 生成的 Python 源代码,在运行 revision 命令的范围内。此功能的主要预期用途是在 Alembic 生成修订文件时对它们运行代码格式化工具,例如 Blackautopep8,以及自定义编写的格式化和 linter 函数。可以配置任意数量的钩子,并且它们将按顺序运行,同时提供新生成的文件的路径以及配置选项。

配置后,后写挂钩会针对生成的修订文件运行,而不管是否使用了自动生成功能。

注意

Alembic 的后写系统部分灵感源自 pre-commit 工具,该工具配置 git 挂钩,在将源文件提交到 git 存储库时对其进行重新格式化。Pre-commit 还可以为 Alembic 修订文件提供此功能,在提交时对其应用代码格式化程序。Alembic 的后写挂钩仅在生成文件后立即对其进行格式化(而不是在提交时)方面有用,并且对于不想使用 pre-commit 的项目也很有用。

基本后处理器配置#

现在,alembic.ini 示例包括带注释的配置,说明如何配置代码格式化工具或其他类似于 linter 的工具,以针对新生成的 file path 运行。示例

[post_write_hooks]

# format using "black"
hooks=black

black.type = console_scripts
black.entrypoint = black
black.options = -l 79

在上面,我们将 hooks 配置为一个名为 "black" 的后写挂钩。请注意,此标签是任意的。然后,我们定义 "black" 后写挂钩的配置,其中包括

  • type - 这是我们正在运行的挂钩类型。Alembic 包含两个挂钩运行程序:"console_scripts",它是一个专门的 Python 函数,使用 subprocess.run() 调用针对修订文件的单独 Python 脚本;以及 "exec",它使用 subprocess.run() 执行任意二进制文件。对于自定义编写的挂钩函数,此配置变量将引用注册自定义挂钩的名称;请参阅下一部分以获取示例。

在 1.12 版中添加: 添加了新的 exec 运行程序

以下配置选项适用于 "console_scripts" 钩子运行器

  • entrypoint - setuptools 入口点 的名称,用于定义控制台脚本。在标准 Python 控制台脚本的范围内,此名称将与通常为代码格式化工具运行的 shell 命令的名称相匹配,在本例中为 black

以下配置选项适用于 "exec" 钩子运行器

  • executable - 要调用的可执行文件的名称。可以是

将在 $PATH 中搜索的裸可执行文件名,或避免路径拦截潜在问题的完整路径名。

以下选项同时受 "console_scripts""exec" 支持

  • options - 将传递给代码格式化工具的一行命令行选项。在本例中,我们希望运行命令 black /path/to/revision.py -l 79。默认情况下,修订路径被定位为第一个参数。为了指定不同的位置,我们可以使用 REVISION_SCRIPT_FILENAME 令牌,如下面的示例所示。

    注意

    确保为脚本提供选项,以便它就地重写输入文件。例如,在运行 autopep8 时,应提供 --in-place 选项

    [post_write_hooks]
    hooks = autopep8
    autopep8.type = console_scripts
    autopep8.entrypoint = autopep8
    autopep8.options = --in-place REVISION_SCRIPT_FILENAME
    
  • cwd - 代码处理工具运行时的可选工作目录。

在运行 alembic revision -m "rev1" 时,我们现在还将看到 black 工具的输出

$ alembic revision -m "rev1"
  Generating /path/to/project/versions/481b13bc369a_rev1.py ... done
  Running post write hook "black" ...
reformatted /path/to/project/versions/481b13bc369a_rev1.py
All done! ✨ 🍰 ✨
1 file reformatted.
  done

钩子也可以指定为名称列表,这些名称对应于将按顺序运行的钩子运行器。例如,我们还可以在运行 black 工具后运行 zimports 导入重写工具(由 Alembic 的作者编写),使用以下配置

[post_write_hooks]

# format using "black", then "zimports"
hooks=black, zimports

black.type = console_scripts
black.entrypoint = black
black.options = -l 79 REVISION_SCRIPT_FILENAME

zimports.type = console_scripts
zimports.entrypoint = zimports
zimports.options = --style google REVISION_SCRIPT_FILENAME

使用上述配置时,新生成的修订文件将首先由“black”工具处理,然后由“zimports”工具处理。

或者,可以按如下方式运行 pre-commit 本身

[post_write_hooks]

hooks = pre-commit

pre-commit.type = console_scripts
pre-commit.entrypoint = pre-commit
pre-commit.options = run --files REVISION_SCRIPT_FILENAME
pre-commit.cwd = %(here)s

(最后一行有助于确保始终可以找到 .pre-commit-config.yaml 文件,无论从何处调用钩子。)

编写自定义挂钩作为 Python 函数#

上一部分说明了如何运行命令行代码格式化程序,通过使用 Alembic 提供的 post write 挂钩,称为 console_scripts。此挂钩实际上是一个 Python 函数,使用注册函数注册在该名称下,该函数还可用于注册其他类型的挂钩。

为了说明这一点,我们将使用一个简短的 Python 函数的示例,该函数希望重写生成的代码以使用制表符而不是四个空格。为简单起见,我们将说明如何直接在 env.py 文件中显示此函数。使用 write_hooks.register() 装饰器声明和注册函数

from alembic.script import write_hooks
import re

@write_hooks.register("spaces_to_tabs")
def convert_spaces_to_tabs(filename, options):
    lines = []
    with open(filename) as file_:
        for line in file_:
            lines.append(
                re.sub(
                    r"^(    )+",
                    lambda m: "\t" * (len(m.group(1)) // 4),
                    line
                )
            )
    with open(filename, "w") as to_write:
        to_write.write("".join(lines))

我们的新 "spaces_to_tabs" 挂钩可以在 alembic.ini 中配置如下

[alembic]

# ...

# ensure the revision command loads env.py
revision_environment = true

[post_write_hooks]

hooks = spaces_to_tabs

spaces_to_tabs.type = spaces_to_tabs

当运行 alembic revision 时,将在所有情况下加载 env.py 文件,将注册自定义“spaces_to_tabs”函数,然后针对新生成的文件路径运行它

$ alembic revision -m "rev1"
  Generating /path/to/project/versions/481b13bc369a_rev1.py ... done
  Running post write hook "spaces_to_tabs" ...
  done

运行 Alembic Check 以测试新的升级操作#

在开发代码时,了解一组代码更改是否对数据库模型进行了任何净更改非常有用,因此需要生成新的修订。为了实现自动化,Alembic 提供了 alembic check 命令。此命令将运行与 alembic revision --autogenerate 相同的过程,直到生成修订文件为止,但不会生成任何新文件。相反,如果检测到新操作将呈现到新修订中,则它将返回错误代码和消息,否则,返回成功代码和消息。当 alembic check 返回成功代码时,这表明 alembic revision --autogenerate 命令只会生成空迁移,不需要运行。

alembic check 可以用于 CI 系统和提交方案,以确保传入的代码不需要生成新的修订版。在下面的示例中,演示了检测新操作的检查

$ alembic check
FAILED: New upgrade operations detected: [
  ('add_column', None, 'my_table', Column('data', String(), table=<my_table>)),
  ('add_column', None, 'my_table', Column('newcol', Integer(), table=<my_table>))]

相比之下,当未检测到新操作时

$ alembic check
No new upgrade operations detected.

在版本 1.9.0 中添加。

注意

alembic check 命令使用与 alembic revision --autogenerate 进程相同的模型比较进程。这意味着诸如 EnvironmentContext.configure.compare_typeEnvironmentContext.configure.compare_server_default 等参数通常有效,并且在运行 alembic check 时,自动生成检测中的限制也相同。