在应用代码中使用Alembic API
我正在使用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 个回答
我没有使用Flask,所以没法用已经推荐的Flask-Alembic库。相反,我经过一番折腾,写了一个简短的函数来运行所有适用的迁移。我把所有与alembic相关的文件放在一个叫做migrations的子模块(文件夹)里。其实我把alembic.ini
和env.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()
这个问题比较宽泛,具体怎么实现你的想法还得靠你自己,不过这是可行的。
你可以在Python代码中直接调用Alembic,而不需要使用命令,因为Alembic本身也是用Python写的!你只需要了解这些命令背后在做什么。
老实说,文档现在还不是很好,因为这个库还处于相对早期的版本,不过只要稍微查找一下,你会发现以下内容:
- 创建一个配置(Config)
- 用这个配置创建一个脚本目录(ScriptDirectory)
- 用配置和脚本目录创建一个环境上下文(EnvironmentContext)
- 用环境上下文创建一个迁移上下文(MigrationContext)
- 大多数命令都是用配置和迁移上下文中的某些方法组合而成的
我写了一个扩展,可以让你通过编程方式在Flask-SQLAlchemy数据库中使用Alembic。这个实现是和Flask以及Flask-SQLAlchemy绑定在一起的,但应该是个不错的起点。在这里查看Flask-Alembic。
关于你提到的如何创建新数据库,你可以用Alembic来创建表,或者先用metadata.create_all()
,然后再用alembic stamp head
(或者等效的Python代码)。我建议你总是使用迁移的方式来创建表,而忽略直接用metadata.create_all()
。
我没有使用过cx_freeze,但只要迁移文件包含在发布包里,并且代码中的目录路径是正确的,应该就没问题。
这里有一个纯粹的编程示例,展示了如何以编程方式配置和调用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表现得非常可控。
如果你查看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()方法,可以让你标记某个数据库为特定版本。上面的示例也可以很容易地调整来调用这个方法。
这是我在把我的软件连接到 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()
的看法 :)