在应用代码中使用Alembic API

82 投票
9 回答
46322 浏览
提问于 2025-04-18 12:26

我正在使用SQLite作为我的PySide桌面应用程序的文件格式(想了解原因可以查看这里)。也就是说,当用户使用我的应用时,他们的数据会保存在他们电脑上的一个数据库文件里。我使用SQLAlchemy这个工具来和数据库进行沟通。

随着我发布新版本的应用,我可能会修改数据库的结构。我不想让用户每次我改变结构时都要丢掉他们的数据,所以我需要把他们的数据库迁移到最新的格式。此外,我经常创建临时数据库,以便保存一些数据的子集,供外部程序使用。我想用alembic来创建这些数据库,这样它们就会标记上正确的版本。

我有几个问题:

  • 有没有办法在我的Python代码里调用alembic?我觉得要用Popen去调用一个纯Python模块有点奇怪,但文档里只是提到从命令行使用alembic。主要是,我需要把数据库的位置改成用户数据库所在的位置。

  • 如果不行,我能不能从命令行指定一个新的数据库位置,而不需要编辑.ini文件?这样通过Popen调用alembic就不会太麻烦了。

  • 我看到alembic在一个叫alembic_version的简单表里保存版本信息,里面有一列叫version_num,只有一行指定版本。我能不能在我的数据库结构里添加一个alembic_version表,并在创建新数据库时填入最新版本,这样就没有额外的开销?这样做合适吗?我是不是应该直接用alembic来创建所有数据库?

我在项目目录下的单个数据库上已经很好地使用了alembic。我想用alembic方便地在任意位置迁移和创建数据库,最好是通过某种Python API,而不是命令行。这个应用程序还用cx_Freeze打包了,如果这有影响的话。

谢谢!

9 个回答

6

我没有使用Flask,所以没法用已经推荐的Flask-Alembic库。相反,我经过一番折腾,写了一个简短的函数来运行所有适用的迁移。我把所有与alembic相关的文件放在一个叫做migrations的子模块(文件夹)里。其实我把alembic.inienv.py放在一起,这可能有点不太常规。下面是我alembic.ini文件的一小段,用来做相应的调整:

[alembic]
script_location = .

接着,我在同一个目录下添加了一个文件,命名为run.py。无论你把脚本放在哪里,只需要修改下面的代码,指向正确的路径就可以了:

from alembic.command import upgrade
from alembic.config import Config
import os


def run_sql_migrations():
    # retrieves the directory that *this* file is in
    migrations_dir = os.path.dirname(os.path.realpath(__file__))
    # this assumes the alembic.ini is also contained in this same directory
    config_file = os.path.join(migrations_dir, "alembic.ini")

    config = Config(file_=config_file)
    config.set_main_option("script_location", migrations_dir)

    # upgrade the database to the latest revision
    upgrade(config, "head")

有了这个run.py文件,我就可以在我的主代码中这样做:

from mymodule.migrations.run import run_sql_migrations


run_sql_migrations()
12

这个问题比较宽泛,具体怎么实现你的想法还得靠你自己,不过这是可行的。

你可以在Python代码中直接调用Alembic,而不需要使用命令,因为Alembic本身也是用Python写的!你只需要了解这些命令背后在做什么。

老实说,文档现在还不是很好,因为这个库还处于相对早期的版本,不过只要稍微查找一下,你会发现以下内容:

  1. 创建一个配置(Config)
  2. 用这个配置创建一个脚本目录(ScriptDirectory)
  3. 用配置和脚本目录创建一个环境上下文(EnvironmentContext)
  4. 用环境上下文创建一个迁移上下文(MigrationContext)
  5. 大多数命令都是用配置和迁移上下文中的某些方法组合而成的

我写了一个扩展,可以让你通过编程方式在Flask-SQLAlchemy数据库中使用Alembic。这个实现是和Flask以及Flask-SQLAlchemy绑定在一起的,但应该是个不错的起点。在这里查看Flask-Alembic。

关于你提到的如何创建新数据库,你可以用Alembic来创建表,或者先用metadata.create_all(),然后再用alembic stamp head(或者等效的Python代码)。我建议你总是使用迁移的方式来创建表,而忽略直接用metadata.create_all()

我没有使用过cx_freeze,但只要迁移文件包含在发布包里,并且代码中的目录路径是正确的,应该就没问题。

13

这里有一个纯粹的编程示例,展示了如何以编程方式配置和调用alembic命令。

目录结构(为了让代码更容易阅读)

.                         # root dir
|- alembic/               # directory with migrations
|- tests/diy_alembic.py   # example script
|- alembic.ini            # ini file

接下来是diy_alembic.py文件

import os
import argparse
from alembic.config import Config
from alembic import command
import inspect

def alembic_set_stamp_head(user_parameter):
    # set the paths values
    this_file_directory = os.path.dirname(os.path.abspath(inspect.stack()[0][1]))
    root_directory      = os.path.join(this_file_directory, '..')
    alembic_directory   = os.path.join(root_directory, 'alembic')
    ini_path            = os.path.join(root_directory, 'alembic.ini')

    # create Alembic config and feed it with paths
    config = Config(ini_path)
    config.set_main_option('script_location', alembic_directory)    
    config.cmd_opts = argparse.Namespace()   # arguments stub

    # If it is required to pass -x parameters to alembic
    x_arg = 'user_parameter=' + user_parameter
    if not hasattr(config.cmd_opts, 'x'):
        if x_arg is not None:
            setattr(config.cmd_opts, 'x', [])
            if isinstance(x_arg, list) or isinstance(x_arg, tuple):
                for x in x_arg:
                    config.cmd_opts.x.append(x)
            else:
                config.cmd_opts.x.append(x_arg)
        else:
            setattr(config.cmd_opts, 'x', None)

    #prepare and run the command
    revision = 'head'
    sql = False
    tag = None
    command.stamp(config, revision, sql=sql, tag=tag)

    #upgrade command
    command.upgrade(config, revision, sql=sql, tag=tag)

这段代码基本上是从这个Flask-Alembic文件中剪切过来的。你可以在这里查看其他命令的用法和细节。

为什么选择这个解决方案? - 这个方案是因为在运行自动化测试时,需要创建alembic的时间戳、升级和降级。

  • os.chdir(migration_directory)会干扰一些测试。
  • 我们希望有一个统一的数据库创建和操作的来源。“如果我们用alembic来创建和管理数据库,那么在测试中也应该使用alembic,而不是metadata.create_all()。”
  • 虽然上面的代码超过了4行,但如果以这种方式使用,alembic表现得非常可控。
31

如果你查看alembic文档中的命令API页面,你会看到一个示例,展示如何直接从Python应用程序中运行CLI命令,而不需要通过CLI代码。

直接运行alembic.config.main有一个缺点,就是会执行env.py脚本,这可能不是你想要的结果。例如,它会修改你的日志配置。

还有一种非常简单的方法,就是使用上面提到的“命令API”。比如,我写了一个小的辅助函数:

from alembic.config import Config
from alembic import command

def run_migrations(script_location: str, dsn: str) -> None:
    LOG.info('Running DB migrations in %r on %r', script_location, dsn)
    alembic_cfg = Config()
    alembic_cfg.set_main_option('script_location', script_location)
    alembic_cfg.set_main_option('sqlalchemy.url', dsn)
    command.upgrade(alembic_cfg, 'head')

在这里,我使用了set_main_option方法,这样如果需要的话,就可以在不同的数据库上运行迁移。所以我可以简单地这样调用:

run_migrations('/path/to/migrations', 'postgresql:///my_database')

至于这两个值(路径和DSN)从哪里来,就看你自己了。但这似乎非常接近你想要实现的目标。命令API还有stamp()方法,可以让你标记某个数据库为特定版本。上面的示例也可以很容易地调整来调用这个方法。

80

这是我在把我的软件连接到 alembic 后学到的东西:

有没有办法在我的Python代码里调用alembic?

有的。到目前为止,alembic的主要入口点是 alembic.config.main,所以你可以导入它并自己调用,比如:

import alembic.config
alembicArgs = [
    '--raiseerr',
    'upgrade', 'head',
]
alembic.config.main(argv=alembicArgs)

需要注意的是,alembic会在当前目录下查找迁移文件(也就是,os.getcwd())。我通过在调用alembic之前使用 os.chdir(migration_directory) 来处理这个问题,但可能还有更好的解决方案。


我可以在命令行中指定新的数据库位置,而不需要编辑.ini文件吗?

可以。关键在于 -x 这个命令行参数。从 alembic -h(令人惊讶的是,我在文档中找不到命令行参数的参考):

optional arguments:
 -x X                  Additional arguments consumed by custom env.py
                       scripts, e.g. -x setting1=somesetting -x
                       setting2=somesetting

所以你可以创建自己的参数,比如 dbPath,然后在 env.py 中拦截它:

alembic -x dbPath=/path/to/sqlite.db upgrade head

然后在 env.py 中,例如:

def run_migrations_online():   
    # get the alembic section of the config file
    ini_section = config.get_section(config.config_ini_section)

    # if a database path was provided, override the one in alembic.ini
    db_path = context.get_x_argument(as_dictionary=True).get('dbPath')
    if db_path:
        ini_section['sqlalchemy.url'] = db_path

    # establish a connectable object as normal
    connectable = engine_from_config(
        ini_section,
        prefix='sqlalchemy.',
        poolclass=pool.NullPool)

    # etc

当然,你也可以通过 argvalembic.config.main 中提供 -x 参数。

我同意 @davidism 关于使用迁移和 metadata.create_all() 的看法 :)

撰写回答