Использование 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! Вам просто нужно воссоздать то, что команды делают за кулисами.

Следует признать, что документы не в очень хорошей форме, поскольку они все еще относительно ранние выпуски библиотеки, но, немного покопавшись, вы найдете следующее:

  1. Создать Конфиг
  2. Используйте Config для создания 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, но все должно быть в порядке, если миграции включены в дистрибутив и путь к этому каталогу в коде указан правильно.

Ответ 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)