Использование API Alembic из внутреннего кода приложения
Я использую SQLite в качестве формата файла приложения (см. здесь, почему вы хотели бы это сделать) для моего настольного приложения на базе PySide. То есть, когда пользователь использует мое приложение, их данные сохраняются в одном файле базы данных на своем компьютере. Я использую ORM SQLAlchemy для связи с базами данных.
Когда я выпускаю новые версии приложения, я могу изменить схему базы данных. Я не хочу, чтобы пользователям приходилось отбрасывать свои данные каждый раз, когда я меняю схему, поэтому мне нужно перенести свои базы данных в самый новый формат. Кроме того, я создаю временные базы данных для сохранения подмножеств данных для использования с некоторыми внешними процессами. Я хочу создать эти базы данных с помощью alembic, чтобы они были помечены соответствующей версией.
У меня есть несколько вопросов:
-
Есть ли способ вызвать alembic из моего кода Python? Я думаю, что странно использовать Popen
для чистого модуля Python, но документы просто используют alembic из командной строки. В основном, мне нужно изменить местоположение базы данных везде, где находится база данных пользователей.
-
Если это невозможно, могу ли я указать новое местоположение базы данных из командной строки без редактирования файла .ini? Это сделало бы вызов alembic через Popen
неважным.
-
Я вижу, что alembic сохраняет свою информацию о версии под простой таблицей alembic_version
с одним столбцом с именем version_num
и одной строкой, указывающей версию. Можно ли добавить таблицу alembic_version
в мою схему и заполнить ее последней версией при создании новых баз данных, чтобы не было накладных расходов? Это даже хорошая идея; должен ли я просто использовать alembic для создания всех баз данных?
У меня хорошая работа для единой базы данных, которую я использую для разработки в моем каталоге проектов. Я хочу использовать alembic для удобной миграции и создания баз данных в произвольных местах, предпочтительно через какой-то Python API, а не в командной строке. Это приложение также заморожено cx_Freeze, если это имеет значение.
Спасибо!
Ответы
Ответ 1
Вот что я узнал после подключения моего программного обеспечения к alembic
:
Есть ли способ вызвать alembic из моего кода Python?
Да. На момент написания этой статьи основной точкой входа для alembic является alembic.config.main
, поэтому вы можете импортировать его и вызывать его самостоятельно, например:
import alembic.config
alembicArgs = [
'--raiseerr',
'upgrade', 'head',
]
alembic.config.main(argv=alembicArgs)
Обратите внимание, что alembic ищет миграции в текущем каталоге (например, os.getcwd()). Я справился с этим с помощью os.chdir(migration_directory)
перед вызовом alembic, но может быть лучшее решение.
Можно ли указать новое местоположение базы данных из командной строки, не редактируя файл .ini?
Да. Ключ лежит в -x
командной строки -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
Конечно, вы можете указать параметр -x, используя argv
в alembic.config.main
.
Я согласен с @davidism по поводу использования миграций против metadata.create_all()
:)
Ответ 2
Это очень широкий вопрос, и на самом деле реализация вашей идеи будет за вами, но это возможно.
Вы можете вызывать Alembic из своего кода Python без использования команд, поскольку он также реализован в Python! Вам просто нужно воссоздать то, что команды делают за кулисами.
Следует признать, что документы не в очень хорошей форме, поскольку они все еще относительно ранние выпуски библиотеки, но, немного покопавшись, вы найдете следующее:
- Создать Конфиг
- Используйте Config для создания ScriptDirectory
- Используйте Config и ScriptDirectory для создания EnvironmentContext
- Используйте EnvironmentContext для создания MigrationContext
- Большинство команд используют некоторую комбинацию методов из Config и MigrationContext
Я написал расширение для предоставления этому программному доступу Alembic к базе данных Flask-SQLAlchemy. Реализация связана с Flask и Flask-SQLAlchemy, но она должна быть хорошим местом для начала. Смотри Flask-Alembic здесь.
Что касается вашего последнего пункта о том, как создавать новые базы данных, вы можете либо использовать Alembic для создания таблиц, либо вы можете использовать metadata.create_all()
затем alembic stamp head
(или эквивалентный код Python). Я рекомендую всегда использовать путь миграции для создания таблиц и игнорировать необработанные metadata.create_all()
.
У меня нет опыта работы с cx_freeze, но все должно быть в порядке, если миграции включены в дистрибутив и путь к этому каталогу в коде указан правильно.
Ответ 3
Вот чисто программный пример того, как конфигурировать и вызывать алембические команды программно.
Настройка каталога (для более удобного чтения кода)
. # 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. Это хорошее место, чтобы посмотреть на использование других команд и детали.
Почему это решение? - Написано было о необходимости создания алембических штампов, апгрейдов и понижений при запуске автоматических тестов.
- os.chdir(igration_directory) вмешивался в некоторые тесты.
- Мы хотели иметь ОДИН источник создания и управления базой данных. "Если мы создаем и управляем базами данных с помощью alembic, то для тестов также будет использоваться оболочка alembic, но не metadata.create_all()".
- Даже если приведенный выше код длиннее 4 строк, alembic показал себя как хороший управляемый зверь, если его так вести.
Ответ 4
Если вы посмотрите на страницу API команд из alembic docs, вы увидите пример того, как запускать команды CLI непосредственно из приложения Python. Не пройдя через код CLI.
Запуск alembic.config.main
имеет недостаток в том, что env.py
скрипт env.py
который может не соответствовать вашим 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(), которые позволяют пометить конкретную БД определенной версии. Приведенный выше пример может быть легко адаптирован для вызова этого.
Ответ 5
Для всех, кто пытался достичь результата в стиле flyway с помощью SQLAlchemy, это сработало для меня:
Добавьте файлigration.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:[email protected]:5432/my_db
Предполагая, что ваши модели SQLAlchemy уже определены, вы можете автоматически сгенерировать ваши сценарии:
alembic revision --autogenerate -m "descriptive migration message"
Если вы получили ошибку о невозможности импортировать вашу модель в env.py, вы можете запустить следующее в своем терминале для исправления
export PYTHONPATH=/path/to/your/project
Наконец, мои сценарии миграции были сгенерированы в каталоге alembic/version, и мне пришлось скопировать их в каталог миграции, чтобы 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
Возможно, у меня что-то неправильно настроено, но сейчас оно работает.
Ответ 6
Я не использую Flask, поэтому я не мог использовать библиотеку Flask-Alembic, которая уже была рекомендована. Вместо этого, немного поработав, я запрограммировал следующую короткую функцию для запуска всех применимых миграций. Я храню все свои файлы, связанные с alembic, в подмодуле (папке), которая называется миграцией. Я на самом деле держу 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()
Ответ 7
Смотрите документацию по alembic.operations.base.Operations:
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)