Использование Alembic API из кода приложения

Я использую SQLite в качестве формата файла приложения (см. здесь, чтобы узнать, почему вам это нужно) для моих основанных на PySide настольное приложение. То есть, когда пользователь использует мое приложение, его данные сохраняются в одном файле базы данных на его компьютере. Я использую SQLAlchemy ORM для связи с базами данных.

По мере выпуска новых версий приложения я могу изменять схему базы данных. Я не хочу, чтобы пользователям приходилось выбрасывать свои данные каждый раз, когда я меняю схему, поэтому мне нужно перенести их базы данных в новейший формат. Кроме того, я часто создаю временные базы данных, чтобы сохранить подмножества данных для использования с некоторыми внешними процессами. Я хочу создать эти базы данных с помощью alembic, чтобы они были помечены правильной версией.

У меня есть несколько вопросов:

  • Есть ли способ вызвать алембик из моего кода Python? Мне кажется странным использовать Popen в чистом модуле Python, но в документации просто используется алембик из командной строки. В основном мне нужно изменить расположение базы данных туда, где находится база данных пользователя.

  • Если это невозможно, могу ли я указать новое расположение базы данных из командной строки, не редактируя файл .ini? Это сделало бы вызов перегонного куба через Popen несложным.

  • Я вижу, что alembic хранит информацию о своей версии в простой таблице с именем alembic_version, с одним столбцом с именем version_num и одной строкой, определяющей версию. Могу ли я добавить alembic_version таблицу в мою схему и заполнить ее последней версией при создании новых баз данных, чтобы избежать накладных расходов? Это вообще хорошая идея; мне просто использовать alembic для создания всех баз данных?

У меня алембик отлично работает с единственной базой данных, которую я использую для разработки в каталоге моего проекта. Я хочу использовать alembic для удобной миграции и создания баз данных в произвольных местах, желательно через какой-то Python API, а не из командной строки. Это приложение также заморожено с помощью cx_Freeze, если это имеет значение.

Спасибо!


person John David Reaver    schedule 08.07.2014    source источник


Ответы (9)


Вот что я узнал после подключения моего программного обеспечения к alembic:

Есть ли способ вызвать алембик из моего кода Python?

да. На момент написания этой статьи основной точкой входа для алембика является _2 _ >, поэтому вы можете импортировать его и назвать его самостоятельно, например:

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

Обратите внимание, что alembic ищет миграции в текущем каталоге (т.е. os.getcwd ()). Я справился с этим, используя os.chdir(migration_directory) перед вызовом alembic, но может быть лучшее решение.


Могу ли я указать новое расположение базы данных из командной строки, не редактируя файл .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

Конечно, вы также можете указать параметр -x, используя argv в alembic.config.main.

Я согласен с @davidism об использовании миграции против metadata.create_all() :)

person ForeverWintr    schedule 04.02.2016
comment
Спасибо за отличный ответ! У меня есть улучшение, которое можно сделать. Вместо того, чтобы ставить ini_section перед соединяемой линией, вы можете добавить config.set_main_option('sqlalchemy.url', db_path) - person Charles L.; 18.10.2018
comment
спасибо, вы сделали мой день :) мое улучшение: вы можете использовать заполнитель% (here) s в alembic.ini, чтобы получить относительный путь из файла alembic.ini. что избавит вас от того, что нужно делать os.chgdir() (найдено здесь: stackoverflow.com/questions/42383400/) - person Yohann; 10.02.2020

Если вы посмотрите на страницу команд API из документации по алембике, вы увидите пример того, как запускать команды CLI непосредственно из приложения Python. Без прохождения кода командной строки.

У запуска 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 команд также имеет штамп () методы, которые позволяют вам пометить данную БД как имеющую определенную версию. Приведенный выше пример можно легко адаптировать для этого.

person exhuma    schedule 28.01.2019
comment
Это решит проблему. Вы также должны указать ini-файл alembic_cfg = Config('alembic.ini') - person lukasz.herok; 25.05.2020
comment
не могли бы вы расширить этот метод, чтобы он не требовал файла env.py? - person BenDog; 08.06.2020
comment
iirc, это уже работает без env-файла. Но я должен дважды проверить. - person exhuma; 08.06.2020

Это очень широкий вопрос, и реализация вашей идеи будет зависеть от вас, но это возможно.

Вы можете вызывать Alembic из своего кода Python без использования команд, поскольку он также реализован на Python! Вам просто нужно воссоздать то, что команды делают за кулисами.

По общему признанию, документы не в очень хорошем состоянии, поскольку это все еще относительно ранние выпуски библиотеки, но, немного покопавшись, вы обнаружите следующее:

  1. Создайте конфигурацию
  2. Используйте конфигурацию для создания ScriptDirectory
  3. Используйте Config и ScriptDirectory для создания EnvironmentContext
  4. Используйте EnvironmentContext для создания MigrationContext
  5. Большинство команд используют комбинацию методов из Config и MigrationContext.

Я написал расширение, чтобы предоставить этому программному Alembic доступ к базе данных Flask-SQLAlchemy. Реализация привязана к Flask и Flask-SQLAlchemy, но с нее должно быть хорошо начать. См. здесь Flask-Alembic.

Что касается вашего последнего замечания о том, как создавать новые базы данных, вы можете либо использовать Alembic для создания таблиц, либо вы можете использовать metadata.create_all(), затем alembic stamp head (или эквивалентный код Python). Я рекомендую всегда использовать путь миграции для создания таблиц и игнорировать необработанные metadata.create_all().

У меня нет опыта работы с cx_freeze, но все должно быть в порядке, если миграции включены в дистрибутив и путь к этому каталогу в коде правильный.

person davidism    schedule 08.07.2014

Вот чисто программный пример того, как программно конфигурировать и вызывать алембические команды.

Настройка каталога (для облегчения чтения кода)

.                         # 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 (migration_directory) мешал некоторым тестам.
  • Мы хотели иметь ОДИН источник для создания базы данных и управления ею. «Если мы создаем базы данных и управляем ими с помощью алембика, то для тестов можно использовать алембик, но не метаданные. Create_all ()».
  • Даже если приведенный выше код длиннее 4 строк, алембик показал себя хорошим управляемым зверем, если его вести таким образом.
person MajesticRa    schedule 20.04.2017
comment
если команда обновления завершится неудачно, будет ли автоматически выполнен откат штампа? - person moshevi; 06.09.2018
comment
почему бы не обновить а потом поставить штамп? - person moshevi; 06.09.2018
comment
В конце концов, мы отказались от перегонного куба. Мы надеялись, что один алембик предоставит нам один источник обновлений для многих диалектов SQL (и поможет со сценариями миграции в качестве приятного дополнения). Но оказывается, что работать со многими простыми чистыми SQL-кодами проще. Даже для дымовых тестов и прочего. - person MajesticRa; 06.09.2018
comment
Что касается вашего вопроса ... Я не помню, есть ли предостережение с запущенным пнем перед обновлением. Но в итоге у нас заработал механизм отката. Приведенный выше код представляет собой своего рода короткую демонстрационную вырезку, где что-то может быть упущено. - person MajesticRa; 06.09.2018
comment
не могли бы вы поделиться механизмом отката? - person moshevi; 12.09.2018

Для всех, кто пытается достичь результата в стиле пролетного пути с помощью 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 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, вы можете запустить следующее в своем терминале fo fix

export PYTHONPATH=/path/to/your/project

Наконец, мои сценарии миграции создавались в каталоге alembic / versions, и мне пришлось скопировать их в каталог миграции, чтобы 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

У меня, наверное, что-то неправильно настроено, но сейчас оно работает.

person Matthew    schedule 06.10.2018

Я не использую Flask, поэтому я не мог использовать уже рекомендованную библиотеку Flask-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()
person soapergem    schedule 22.01.2019

См. Документацию по 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)
person moomima    schedule 08.10.2018
comment
Со своего веб-сайта (alembic.sqlalchemy.org/en/latest/api/runtime. html): для большинства случаев использования API, кроме полномасштабного вызова сценариев миграции, объекты MigrationContext и ScriptDirectory можно создавать и использовать напрямую. Объект EnvironmentContext нужен только тогда, когда вам действительно нужно вызвать модуль env.py, присутствующий в среде миграции. - person craigsparks; 16.07.2019

Не совсем ответ, но мне это было нелегко, поэтому я хотел поделиться:

Как программно передать x_argument с помощью alembic.command.upgrade:

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}

person Yohann    schedule 18.09.2020

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:])
person thnee    schedule 30.03.2021