对 Alembic 升级进行 dry-运行

Do a dry-run of an Alembic upgrade

有时 alembic upgrade head 可能会在 运行 时对我的生产数据库失败,即使它对我的测试数据库运行良好。例如,迁移可能会向我的测试环境中以前不包含 NULL 的列添加 NOT NULL 约束,但 did 包含 NULL正在制作中。

规划部署时,最好能够在 运行 迁移之前检查它是否能够干净利落地应用。对于像 MySQL 这样不支持事务性 DDL(在事务中进行模式更改)的数据库,这大概是不可能的,但原则上对于 do 支持的数据库应该是可能的事务性 DDL,如 PostgreSQL; Alembic 可以尝试在事务中执行升级,然后将其回滚。

(一个警告:这是一个不完美的解决方案,因为 PostgreSQL 允许某些约束为 DEFERRED,这意味着它们在您提交之前不会被检查。检查这些的 dry-运行 是,我想,如果不创建数据库副本是不可能的。但是,执行 DDL 和回滚方法总比没有好。)

Alembic 是否支持此类功能?如果没有,是否有一些 hacky 方法来实现它?

您可以创建自己的 alembic 配置、脚本目录和环境上下文。至关重要的是,这应该有一个已经创建的连接传递给它,您可以在完成 dry-运行.

后回滚
engine = sa.create_engine(url)
session = Session(bind=engine)
try:
    alembic_config = AlembicConfig(config_args={...})
    alembic_config.attributes.update({"connection": session.connection()})
    script_directory = ScriptDirectory.from_config(alembic_config)
    env_context = EnvironmentContext(
        alembic_config,
        script_directory,
        fn=lambda rev, context: script_directory._upgrade_revs("head", rev),
        as_sql=False,
    )
    env_context.configure(connection=session.connection())
    env_context.run_migrations()

finally:
    session.rollback()
    session.close()

实现这一点的一个简单技巧是在 env.py 中的 run_migrations_online 函数中注入一个条件回滚,只有当某些标志出现时才会触发,表明我们想要一个 dry-运行.

如果你的已经被修改,请回想一下 alembic init 创建的 run_migrations_online 函数的默认实现如下所示:

def run_migrations_online():
    """Run migrations in 'online' mode.

    In this scenario we need to create an Engine
    and associate a connection with the context.

    """
    connectable = engine_from_config(
        config.get_section(config.config_ini_section),
        prefix="sqlalchemy.",
        poolclass=pool.NullPool,
    )

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

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

注意:

  • context.begin_transaction()__enter__ 方法——我们已经在默认实现中调用了它——为我们提供了一个带有 rollback() 方法的交易对象,if后端使用事务性 DDL,或者如果事务性 ddl 被强制使用 transactional_ddl 标志
  • 我们的 context 对象有一个 get_x_argument 方法,我们可以使用它来支持将自定义参数传递给 alembic 命令。

因此,通过以下小改动(除了添加 as transaction 和最后三行之外,下面的所有内容都是相同的)我们可以拥有我们的 dry-运行 功能:

def run_migrations_online():
    """Run migrations in 'online' mode.

    In this scenario we need to create an Engine
    and associate a connection with the context.

    """
    connectable = engine_from_config(
        config.get_section(config.config_ini_section),
        prefix="sqlalchemy.",
        poolclass=pool.NullPool,

        # ensure the context will create a transaction
        # for backends that dont normally use transactional DDL.
        # note that ROLLBACK will not roll back DDL structures
        # on databases such as MySQL, as well as with SQLite
        # Python driver's default settings
        transactional_ddl=True,
    )

    with connectable.connect() as connection:
        context.configure(
            connection=connection, target_metadata=target_metadata
        )
        with context.begin_transaction() as transaction:
            context.run_migrations()
            if 'dry-run' in context.get_x_argument():
                print('Dry-run succeeded; now rolling back transaction...')
                transaction.rollback()

现在,做一个干-运行,做:

alembic -x dry-run upgrade head

并做一个真正的 运行,只需做:

alembic upgrade head

和以前一样。