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

2jcobegt  于 2023-01-31  发布在  SQLite
关注(0)|答案(9)|浏览(190)

我正在使用SQLite作为我的基于PySide的桌面应用程序的应用程序文件格式(请参阅here了解为什么要这样做)。也就是说,当用户使用我的应用程序时,他们的数据将保存在他们机器上的单个数据库文件中。我正在使用SQLAlchemy ORM与数据库通信。
当我发布新版本的应用程序时,我可能会修改数据库架构。我不希望用户在我每次更改架构时都必须丢弃他们的数据,所以我需要将他们的数据库迁移到最新的格式。此外,我创建了很多临时数据库来保存数据的子集以供一些外部进程使用。我想用alembic创建这些数据库,这样它们就可以用正确的版本进行标记。
我有几个问题:

  • 有没有办法从Python代码内部调用alembic?我觉得在纯Python模块中使用Popen有点奇怪,但文档只是从命令行使用alembic。主要是,我需要将数据库位置更改为用户数据库所在的位置。
  • 如果这不可能,我可以从命令行指定一个新的数据库位置,而不编辑. ini文件吗?这将使通过Popen调用alembic不是什么大问题。
  • 我看到alembic将其版本信息保存在一个名为alembic_version的简单表中,该表有一列名为version_num,还有一行指定版本,我是否可以在创建新数据库时向模式中添加一个alembic_version表,并使用最新版本填充它,这样就没有开销了?我应该用alembic来创建所有的数据库吗?

我让alembic在我的项目目录中为我用来开发的单个数据库工作得很好。我想使用alembic在任意位置方便地迁移和创建数据库,最好是通过某种Python API,而不是命令行。这个应用程序也用cx_Freeze冻结,以防有什么不同。
谢谢!

368yc8dk

368yc8dk1#

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

当然,您也可以在alembic.config.main中使用argv提供-x参数。
我同意@davidism关于使用迁移vs metadata.create_all()的观点:)

z4bn682m

z4bn682m2#

如果你看一下alembic文档中的commands 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方法是为了能够在需要时在不同的DB上运行迁移。

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

从哪里获取这两个值(路径和DSN)取决于你自己。但这似乎非常接近你想要实现的。命令API也有stamp()方法,它允许你标记一个给定的DB为特定的版本。上面的例子可以很容易地修改来调用它。

ax6ht2ek

ax6ht2ek3#

这是一个非常宽泛的问题,实际上实现你的想法将取决于你,但这是可能的。
你可以从Python代码中调用Alembic,而不需要使用命令,因为它也是用Python实现的!你只需要重新创建命令在幕后所做的事情。
诚然,文档的状态不是很好,因为它们仍然是相对较早的库版本,但稍加挖掘,您将发现以下内容:
1.创建Config
1.使用配置创建ScriptDirectory
1.使用Config和ScriptDirectory创建环境上下文
1.使用环境上下文创建迁移上下文
1.大多数命令使用Config和MigrationContext中方法的某种组合
我已经编写了一个扩展来提供对Flask-SQLAlchemy数据库的这种编程式Alembic访问,该实现与Flask和Flask-SQLAlchemy绑定,但应该是一个很好的起点。
关于如何创建新数据库的最后一点,可以使用Alembic创建表,也可以先使用metadata.create_all()再使用alembic stamp head(或等效的python代码),我建议始终使用迁移路径创建表,而忽略原始的metadata.create_all()
我对cx_freeze没有任何经验,但是只要迁移包含在分发版中并且代码中指向该目录的路径正确,它就应该没问题。

4dbbbstv

4dbbbstv4#

下面是一个纯编程的示例,说明如何以编程方式配置和调用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中截取的。它是查看其他命令用法和细节的好地方。

为什么选择此解决方案?-编写此解决方案时需要创建一个alembic stamp,并在运行自动化测试时进行升级和降级。

  • os.chdir(迁移目录)干扰了某些测试。
  • 我们希望有一个数据库创建和操作的来源。“如果我们用alembic创建和管理数据库,alembic而不是metadata。create_all()shell也可以用于测试”。
  • 即使上面的代码超过4行,alembic也显示出它是一个很好的可控制的野兽。
d6kp6zgx

d6kp6zgx5#

我没有使用Flask,所以我不能使用已经推荐的Flask-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()
bvuwiixz

bvuwiixz6#

参见alembic.operations.base.操作的文档:

from alembic.runtime.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)
gt0wga4j

gt0wga4j7#

对于其他任何试图用SQLAlchemy实现flyway风格的结果的人来说,这对我很有效:
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

更新www.example.comenv.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"

如果您收到无法在www.example.com中导入模型的错误,您可以在终端中运行以下命令来修复env.py, you can run the following in your terminal fo fix

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

我可能有一些配置错误,但它现在工作。

zc0qhyus

zc0qhyus8#

Alembic将其所有命令公开为alembic.command下的可导入调用项。
https://alembic.sqlalchemy.org/en/latest/api/commands.html
我编写了这个 Package 器,这样我就可以通过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:])
x4shl7ld

x4shl7ld9#

这不是一个真正的答案,但我很难回答,所以我想分享:
如何使用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}

相关问题