从应用程序内部代码中使用Alembic API

时间:2014-07-08 00:57:00

标签: python sqlite sqlalchemy alembic

对于基于PySide的桌面应用程序,我使用SQLite作为应用程序文件格式(请参阅here了解您为什么要这样做)。也就是说,当用户使用我的应用程序时,他们的数据将保存在其计算机上的单个数据库文件中。我正在使用SQLAlchemy ORM与数据库进行通信。

当我发布应用程序的新版本时,我可能会修改数据库架构。我不希望用户每次更改架构时都要丢弃他们的数据,所以我需要将他们的数据库迁移到最新的格式。此外,我创建了很多临时数据库来保存数据的子集,以便与某些外部进程一起使用。我想用alembic创建这些数据库,因此它们被标记为正确的版本。

我有几个问题:

  • 有没有办法从我的Python代码中调用alembic?我认为必须将Popen用于纯Python模块是很奇怪的,但是文档只是从命令行使用alembic。主要是,我需要将数据库位置更改为用户数据库所在的位置。

  • 如果无法做到这一点,是否可以在不编辑.ini文件的情况下从命令行指定新的数据库位置?这样就可以通过Popen调用alembic并不是什么大问题。

  • 我看到alembic将其版本信息保存在一个名为alembic_version的简单表下,其中一列名为version_num,一行指定版本。我可以在我的架构中添加alembic_version表,并在创建新数据库时使用最新版本填充它,这样就没有开销吗?这甚至是个好主意;我应该只使用alembic来创建所有数据库吗?

我的alembic非常适合我在项目目录中使用的单个数据库。我想使用alembic在任意位置方便地迁移和创建数据库,最好是通过某种Python API,而不是命令行。此应用程序也会被cx_Freeze冻结,以防万一。

谢谢!

9 个答案:

答案 0 :(得分:22)

以下是我将软件连接到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

当然,您也可以使用argv中的alembic.config.main来提供-x参数。

我同意@davidism关于使用迁移与metadata.create_all():)

答案 1 :(得分:10)

这是一个非常广泛的问题,实际上实现你的想法将取决于你,但它是可能的。

您可以在不使用命令的情况下从Python代码调用Alembic,因为它也是用Python实现的!您只需要重新创建命令在幕后执行的操作。

不可否认,这些文档的形式并不是很好,因为这些文档仍然是相对较早的版本库,但只需稍加挖掘,您就会发现以下内容:

  1. 创建Config
  2. 使用配置创建ScriptDirectory
  3. 使用Config和ScriptDirectory创建EnvironmentContext
  4. 使用EnvironmentContext创建MigrationContext
  5. 大多数命令使用Config和MigrationContext
  6. 中的某些方法组合

    我已经编写了一个扩展来为Flask-SQLAlchemy数据库提供这种程序化的Alembic访问。实现与Flask和Flask-SQLAlchemy相关联,但它应该是一个很好的起点。 See Flask-Alembic here.

    关于如何创建新数据库的最后一点,您可以使用Alembic创建表,也可以使用metadata.create_all()然后使用alembic stamp head(或等效的python代码)。我建议始终使用迁移路径来创建表,并忽略原始metadata.create_all()

    我对cx_freeze没有任何经验,但只要迁移包含在发行版中并且代码中该目录的路径正确,它就应该没问题。

答案 2 :(得分:6)

以下是如何以编程方式配置和调用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)

代码或多或少是this Flask-Alembic file的缩减。这是查看其他命令用法和详细信息的好地方。

为什么选择这个解决方案? - 这是为了在运行自动化测试时需要创建一个简化的邮票,升级和降级。

  • os.chdir(migration_directory)干扰了一些测试。
  • 我们希望拥有一个数据库创建和操作源。 “如果我们用alembic来创建和管理数据库,那么也可以使用alembic而不是metadata.create_all()shell进行测试”。
  • 即使上面的代码超过4行,如果以这种方式驱动,alembic也会表现为一个好的可控制的野兽。

答案 3 :(得分:3)

如果您从Alembic文档中查看commands API页,则会看到一个示例,该示例说明了如何直接从Python应用程序中运行CLI命令。无需通过CLI代码。

运行#registration的缺点是执行了alembic.config.main脚本,这可能不是您想要的。例如,它将修改您的日志记录配置。

另一种非常简单的方法是使用上面链接的“命令API”。例如,这是我最终编写的一个小辅助函数:

env.py

我在这里使用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

由您决定从哪里获取这两个值(路径和DSN)。但这似乎与您想要实现的目标非常接近。命令API还具有stamp()方法,可用于将给定的DB标记为特定版本。上面的示例可以很容易地调用它。

答案 4 :(得分:2)

对于其他尝试使用SQLAlchemy实现类似飞行的结果的人,这对我有用:

migration.py 添加到您的项目:

from flask_alembic import Alembic

def migrate(app):
    alembic = Alembic()
    alembic.init_app(app)
    with app.app_context():
        alembic.upgrade()

初始化数据库后,在应用程序启动时调用它

application = Flask(__name__)
db = SQLAlchemy()
db.init_app(application)
migration.migrate(application)

然后,您只需要执行其余的标准Alembic步骤:

将您的项目初始化为淡啤酒色

alembic init alembic

更新env.py:

from models import MyModel
target_metadata = [MyModel.Base.metadata]

更新alembic.ini

sqlalchemy.url = postgresql://postgres:postgres@localhost:5432/my_db

假设您已经定义了SQLAlchemy模型,则可以立即自动生成脚本:

alembic revision --autogenerate -m "descriptive migration message"

如果在无法将模型导入到env.py中时遇到错误,可以在终端修复程序中运行以下命令

export PYTHONPATH=/path/to/your/project

最后,我的迁移脚本是在alembic / versions目录中生成的,我必须将它们复制到migrations目录中,以便Alembic能够将其提取。

├── alembic
│   ├── env.py
│   ├── README
│   ├── script.py.mako
│   └── versions
│       ├── a5402f383da8_01_init.py  # generated here...
│       └── __pycache__
├── alembic.ini
├── migrations
│   ├── a5402f383da8_01_init.py  # manually copied here
│   └── script.py.mako

我可能配置错误,但是现在可以正常工作。

答案 5 :(得分:1)

我没有使用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()

答案 6 :(得分:0)

请参阅alembic.operations.base.Operations文档:

    from alembic.migration import MigrationContext
    from alembic.operations import Operations

    conn = myengine.connect()
    ctx = MigrationContext.configure(conn)
    op = Operations(ctx)

    op.alter_column("t", "c", nullable=True)

答案 7 :(得分:0)

这不是一个真正的答案,但是我对此感到很难受,所以我想分享一下:

如何使用alembic.command.upgrade以编程方式传递x_argument:

class CmdOpts:
    x = {"data=true"}

在这里data = true是我在命令行中作为x_argument传递的内容

    alembic_config = AlembicConfig(ini_location)
    setattr(alembic_config, "cmd_opts", CmdOpts())
    alembic_config.cmd_opts.x = {"data": True}

答案 8 :(得分:0)

Alembic 将其所有命令公开为 alembic.command 下的可导入调用。

https://alembic.sqlalchemy.org/en/latest/api/commands.html

我编写了这个包装器,只是为了能够通过 python 代码设置自定义日志记录。

import logging

import alembic.command
import alembic.config

from somewhere import config_logging


def run():
    config_logging()

    log = logging.getLogger(__name__)

    if len(sys.argv) < 3:
        log.error("command must be specified")
        exit(1)

    else:
        command_name = sys.argv[2]

    try:
        command = getattr(alembic.command, name)

    except AttributeError:
        log.error(f"{name} is not a valid alembic command")
        exit(2)

    config = alembic.config.Config()
    config.set_main_option("script_location", "path/to/alembic")
    config.set_main_option("sqlalchemy.url", "postgres://...")

    command(config, *sys.argv[3:])