Comment exécuter des insertions et des mises à jour dans un script de mise à niveau Alembic?

95

J'ai besoin de modifier les données lors d'une mise à niveau d'Alembic.

J'ai actuellement une table des 'joueurs' dans une première révision:

def upgrade():
    op.create_table('player',
        sa.Column('id', sa.Integer(), nullable=False),
        sa.Column('name', sa.Unicode(length=200), nullable=False),
        sa.Column('position', sa.Unicode(length=200), nullable=True),
        sa.Column('team', sa.Unicode(length=100), nullable=True)
        sa.PrimaryKeyConstraint('id')
    )

Je veux introduire une table «équipes». J'ai créé une deuxième révision:

def upgrade():
    op.create_table('teams',
        sa.Column('id', sa.Integer(), nullable=False),
        sa.Column('name', sa.String(length=80), nullable=False)
    )
    op.add_column('players', sa.Column('team_id', sa.Integer(), nullable=False))

J'aimerais que la deuxième migration ajoute également les données suivantes:

  1. Remplir le tableau des équipes:

    INSERT INTO teams (name) SELECT DISTINCT team FROM players;
  2. Mettez à jour players.team_id en fonction du nom de players.team:

    UPDATE players AS p JOIN teams AS t SET p.team_id = t.id WHERE p.team = t.name;

Comment exécuter des insertions et des mises à jour dans le script de mise à niveau?

Arek S
la source

Réponses:

147

Ce que vous demandez, c'est une migration de données , par opposition à la migration de schéma qui est la plus répandue dans la documentation Alembic.

Cette réponse suppose que vous utilisez déclarative (par opposition à class-Mapper-Table ou core) pour définir vos modèles. Il devrait être relativement simple de l'adapter aux autres formes.

Notez qu'Alembic fournit quelques fonctions de données de base: op.bulk_insert()et op.execute(). Si les opérations sont assez minimes, utilisez-les. Si la migration nécessite des relations ou d'autres interactions complexes, je préfère utiliser toute la puissance des modèles et des sessions comme décrit ci-dessous.

Voici un exemple de script de migration qui configure certains modèles déclaratifs qui seront utilisés pour manipuler les données dans une session. Les points clés sont:

  1. Définissez les modèles de base dont vous avez besoin, avec les colonnes dont vous aurez besoin. Vous n'avez pas besoin de toutes les colonnes, seulement de la clé primaire et de celles que vous utiliserez.

  2. Dans la fonction de mise à niveau, utilisez op.get_bind()pour obtenir la connexion actuelle et créer une session avec elle.

    • Ou utilisez bind.execute()pour utiliser le niveau inférieur de SQLAlchemy pour écrire directement des requêtes SQL. Ceci est utile pour les migrations simples.
  3. Utilisez les modèles et la session comme vous le feriez normalement dans votre application.

"""create teams table

Revision ID: 169ad57156f0
Revises: 29b4c2bfce6d
Create Date: 2014-06-25 09:00:06.784170
"""

revision = '169ad57156f0'
down_revision = '29b4c2bfce6d'

from alembic import op
import sqlalchemy as sa
from sqlalchemy import orm
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()


class Player(Base):
    __tablename__ = 'players'

    id = sa.Column(sa.Integer, primary_key=True)
    name = sa.Column(sa.String, nullable=False)
    team_name = sa.Column('team', sa.String, nullable=False)
    team_id = sa.Column(sa.Integer, sa.ForeignKey('teams.id'), nullable=False)

    team = orm.relationship('Team', backref='players')


class Team(Base):
    __tablename__ = 'teams'

    id = sa.Column(sa.Integer, primary_key=True)
    name = sa.Column(sa.String, nullable=False, unique=True)


def upgrade():
    bind = op.get_bind()
    session = orm.Session(bind=bind)

    # create the teams table and the players.team_id column
    Team.__table__.create(bind)
    op.add_column('players', sa.Column('team_id', sa.ForeignKey('teams.id'), nullable=False)

    # create teams for each team name
    teams = {name: Team(name=name) for name in session.query(Player.team).distinct()}
    session.add_all(teams.values())

    # set player team based on team name
    for player in session.query(Player):
        player.team = teams[player.team_name]

    session.commit()

    # don't need team name now that team relationship is set
    op.drop_column('players', 'team')


def downgrade():
    bind = op.get_bind()
    session = orm.Session(bind=bind)

    # re-add the players.team column
    op.add_column('players', sa.Column('team', sa.String, nullable=False)

    # set players.team based on team relationship
    for player in session.query(Player):
        player.team_name = player.team.name

    session.commit()

    op.drop_column('players', 'team_id')
    op.drop_table('teams')

La migration définit des modèles distincts car les modèles de votre code représentent l' état actuel de la base de données, tandis que les migrations représentent les étapes en cours de route . Votre base de données peut être dans n'importe quel état le long de ce chemin, de sorte que les modèles peuvent ne pas encore se synchroniser avec la base de données. Sauf si vous êtes très prudent, l'utilisation directe des modèles réels entraînera des problèmes de colonnes manquantes, de données invalides, etc. Il est plus clair d'indiquer explicitement les colonnes et les modèles que vous utiliserez dans la migration.

davidisme
la source
11

Vous pouvez également utiliser Direct SQL voir ( Alembic Operation Reference ) comme dans l'exemple suivant:

from alembic import op

# revision identifiers, used by Alembic.
revision = '1ce7873ac4ced2'
down_revision = '1cea0ac4ced2'
branch_labels = None
depends_on = None


def upgrade():
    # ### commands made by andrew ###
    op.execute('UPDATE STOCK SET IN_STOCK = -1 WHERE IN_STOCK IS NULL')
    # ### end Alembic commands ###


def downgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    pass
    # ### end Alembic commands ###
Martlark
la source
Dans le cas où j'ai toujours voulu lire une instruction SQL à partir d' un fichier externe et passer ensuite à op.executeen upgrade(), est - il un moyen de fournir un modèle par défaut à utiliser par alembic revisioncommande (un corps par défaut du produit .pyfichier)?
Quentin le
1
Je ne sais pas @Quentin. C'est une idée intéressante.
Martlark
6

Je recommande d'utiliser les instructions de base de SQLAlchemy en utilisant une table ad-hoc, comme détaillé dans la documentation officielle , car elle permet l'utilisation de SQL agnostique et de l'écriture pythonique et est également autonome. SQLAlchemy Core est le meilleur des deux mondes pour les scripts de migration.

Voici un exemple du concept:

from sqlalchemy.sql import table, column
from sqlalchemy import String
from alembic import op

account = table('account',
    column('name', String)
)
op.execute(
    account.update().\\
    where(account.c.name==op.inline_literal('account 1')).\\
        values({'name':op.inline_literal('account 2')})
        )

# If insert is required
from sqlalchemy.sql import insert
from sqlalchemy import orm

session = orm.Session(bind=bind)
bind = op.get_bind()

data = {
    "name": "John",
}
ret = session.execute(insert(account).values(data))
# for use in other insert calls
account_id = ret.lastrowid
cmc
la source