web-dev-qa-db-fra.com

Utilisation de l'API Alembic à partir du code d'application interne

J'utilise SQLite comme format de fichier d'application (voir ici pour savoir pourquoi vous souhaitez le faire) pour mon application de bureau basée sur PySide. Autrement dit, lorsqu'un utilisateur utilise mon application, ses données sont enregistrées dans un seul fichier de base de données sur sa machine. J'utilise l'ORM SQLAlchemy pour communiquer avec les bases de données.

Lorsque je publie de nouvelles versions de l'application, je peux modifier le schéma de la base de données. Je ne veux pas que les utilisateurs doivent jeter leurs données à chaque fois que je modifie le schéma, j'ai donc besoin de migrer leurs bases de données vers le format le plus récent. De plus, je crée beaucoup de bases de données temporaires pour enregistrer des sous-ensembles de données à utiliser avec certains processus externes. Je veux créer ces bases de données avec alambic afin qu'elles soient étiquetées avec la bonne version.

J'ai quelques questions:

  • Existe-t-il un moyen d'appeler alambic depuis mon Python? Je pense que c'est bizarre d'avoir à utiliser Popen vers un pur Python , mais les documents utilisent simplement alembic à partir de la ligne de commande. Je dois principalement modifier l'emplacement de la base de données à l'emplacement de la base de données de l'utilisateur.

  • Si ce n'est pas possible, puis-je spécifier un nouvel emplacement de base de données à partir de la ligne de commande sans modifier le fichier .ini? Ainsi, appeler alambic via Popen ne serait pas un gros problème.

  • Je vois que alembic conserve ses informations de version sous une simple table appelée alembic_version, avec une colonne appelée version_num et une seule ligne spécifiant la version. Puis-je ajouter un alembic_version table à mon schéma et remplissez-la avec la dernière version lorsque je crée de nouvelles bases de données afin qu'il n'y ait pas de surcharge? Est-ce même une bonne idée; dois-je utiliser alambic pour créer toutes les bases de données?

J'ai un alambic qui fonctionne très bien pour la base de données unique que j'utilise pour développer avec dans le répertoire de mon projet. Je veux utiliser alembic pour migrer et créer facilement des bases de données dans des emplacements arbitraires, de préférence via une sorte d'API Python, et non la ligne de commande. Cette application est également gelée avec cx_Freeze, au cas où cela rendrait une différence.

Merci!

38
John David Reaver

Voici ce que j'ai appris après avoir connecté mon logiciel à alembic:

Existe-t-il un moyen d'appeler l'alambic depuis mon code Python?

Oui. Au moment de la rédaction de cet article, le principal point d'entrée pour l'alambic est alembic.config.main , vous pouvez donc l'importer et l'appeler vous-même, par exemple:

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

Notez que alembic recherche les migrations dans le répertoire courant (c'est-à-dire os.getcwd ()). J'ai géré cela en utilisant os.chdir(migration_directory) avant d'appeler alembic, mais il peut y avoir une meilleure solution.


Puis-je spécifier un nouvel emplacement de base de données à partir de la ligne de commande sans modifier le fichier .ini?

Oui. La clé réside dans l'argument de ligne de commande -x. Depuis alembic -h (Étonnamment, je n'ai pas pu trouver de référence d'argument de ligne de commande dans les documents):

optional arguments:
 -x X                  Additional arguments consumed by custom env.py
                       scripts, e.g. -x setting1=somesetting -x
                       setting2=somesetting

Vous pouvez donc créer votre propre paramètre, par ex. dbPath, puis interceptez-le dans env.py:

alembic -x dbPath=/path/to/sqlite.db upgrade head

puis par exemple dans 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

Bien sûr, vous pouvez également fournir le paramètre -x en utilisant argv dans alembic.config.main.

Je suis d'accord avec @ davidism sur l'utilisation des migrations vs metadata.create_all() :)

33
ForeverWintr

C'est une question très large, et la mise en œuvre de votre idée dépendra de vous, mais c'est possible.

Vous pouvez appeler Alembic à partir de votre code Python sans utiliser les commandes, car il est également implémenté dans Python! Vous avez juste besoin de recréer ce que les commandes font en arrière-plan.

Certes, les documents ne sont pas en très bonne forme car ce sont encore des versions relativement tôt de la bibliothèque, mais avec un peu de fouille, vous trouverez ce qui suit:

  1. Créez un Config
  2. Utilisez la configuration pour créer un ScriptDirectory
  3. Utilisez la configuration et le ScriptDirectory pour créer un EnvironmentContext
  4. Utilisez le EnvironmentContext pour créer un MigrationContext
  5. La plupart des commandes utilisent une combinaison de méthodes de Config et MigrationContext

J'ai écrit une extension pour fournir cet accès Alembic programmatique à une base de données Flask-SQLAlchemy. L'implémentation est liée à Flask et Flask-SQLAlchemy, mais cela devrait être un bon point de départ. Voir Flask-Alembic ici.

En ce qui concerne votre dernier point sur la façon de créer de nouvelles bases de données, vous pouvez soit utiliser Alembic pour créer les tables, soit utiliser metadata.create_all() puis alembic stamp head (Ou un code python équivalent _ ). Je recommande de toujours utiliser le chemin de migration pour créer les tables et d'ignorer la metadata.create_all() brute.

Je n'ai aucune expérience avec cx_freeze, mais ça devrait aller tant que les migrations sont incluses dans la distribution et que le chemin vers ce répertoire dans le code est correct.

11
davidism

Si vous regardez la page API de commandes des documents alembic, vous voyez un exemple de la façon d'exécuter les commandes CLI directement à partir d'une application Python. Sans passer par l'interface CLI code.

Fonctionnement alembic.config.main a l'inconvénient que le env.py le script est exécuté, ce qui n'est peut-être pas ce que vous voulez. Par exemple, cela modifiera votre configuration de journalisation.

Un autre moyen très simple consiste à utiliser l '"API de commande" liée ci-dessus. Par exemple, voici une petite fonction d'aide que j'ai fini par écrire:

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')

J'utilise le set_main_option méthode ici pour pouvoir exécuter les migrations sur une autre base de données si nécessaire. Je peux donc simplement appeler cela comme suit:

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

L'origine de ces deux valeurs (chemin et DSN) dépend de vous. Mais cela semble être très proche de ce que vous voulez réaliser. L'API de commandes possède également les méthodes stamp () qui vous permettent de marquer une base de données donnée comme étant d'une version spécifique. L'exemple ci-dessus peut être facilement adapté pour appeler cela.

9
exhuma

Voici un exemple purement programmatique de la façon de configurer et d'appeler des commandes alembic par programmation.

La configuration du répertoire (pour faciliter la lecture du code)

.                         # root dir
|- alembic/               # directory with migrations
|- tests/diy_alembic.py   # example script
|- alembic.ini            # ini file

Et voici 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)

Le code est plus ou moins une coupure de ce fichier Flask-Alembic . C'est un bon endroit pour regarder l'utilisation et les détails des autres commandes.

Pourquoi cette solution? - Elle a été écrite dans le besoin de créer un tampon alambic, des mises à niveau et des déclassements lors de l'exécution de tests automatisés.

  • os.chdir (répertoire_migration) a interféré avec certains tests.
  • Nous voulions avoir UNE source de création et de manipulation de bases de données. "Si nous créons et gérons des bases de données avec alembic, alembic mais pas metadata.create_all () Shell sera également utilisé pour les tests".
  • Même si le code ci-dessus est plus long que 4 lignes, l'alambic s'est montré comme une bonne bête contrôlable s'il était conduit de cette façon.
8
MajesticRa

Je n'utilise pas Flask donc je ne pouvais pas utiliser la bibliothèque Flask-Alembic qui était déjà recommandée. Au lieu de cela après un peu de bricolage, j'ai codé la fonction courte suivante pour exécuter tout des migrations applicables. Je garde tous mes fichiers liés à l'alambic sous un sous-module (dossier) appelé migrations. Je garde en fait le alembic.ini avec la env.py, ce qui est peut-être un peu peu orthodoxe. Voici un extrait de mon alembic.ini fichier à ajuster pour cela:

[alembic]
script_location = .

Ensuite, j'ai ajouté le fichier suivant dans le même répertoire et je l'ai nommé run.py. Mais partout où vous conservez vos scripts, il vous suffit de modifier le code ci-dessous pour pointer vers les bons chemins:

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")

Puis avec ça run.py fichier en place, cela me permet de le faire dans mon code principal:

from mymodule.migrations.run import run_sql_migrations


run_sql_migrations()
2
soapergem

Pour toute autre personne essayant d'obtenir un résultat de voie de migration avec SQLAlchemy, cela a fonctionné pour moi:

Ajoutez migration.py à votre projet:

from flask_alembic import Alembic

def migrate(app):
    alembic = Alembic()
    alembic.init_app(app)
    with app.app_context():
        alembic.upgrade()

Appelez-le au démarrage de l'application après l'initialisation de votre base de données

application = Flask(__name__)
db = SQLAlchemy()
db.init_app(application)
migration.migrate(application)

Ensuite, il vous suffit de faire le reste des étapes d'alambic standard:

Initialisez votre projet en tant qu'alambic

alembic init alembic

Mettre à jour env.py:

from models import MyModel
target_metadata = [MyModel.Base.metadata]

Mettre à jour alembic.ini

sqlalchemy.url = postgresql://postgres:postgres@localhost:5432/my_db

En supposant que vos modèles SQLAlchemy sont déjà définis, vous pouvez générer automatiquement vos scripts maintenant:

alembic revision --autogenerate -m "descriptive migration message"

Si vous obtenez une erreur sur l'impossibilité d'importer votre modèle dans env.py, vous pouvez exécuter ce qui suit dans votre terminal pour corriger

export PYTHONPATH=/path/to/your/project

Enfin, mes scripts de migration étaient générés dans le répertoire alembic/versions, et j'ai dû les copier dans le répertoire migrations pour que alembic les récupère.

├── 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

J'ai probablement quelque chose de mal configuré, mais cela fonctionne maintenant.

2
Matthew

Voir la documentation de 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)
1
moomima