Come si eseguono inserimenti e aggiornamenti in uno script di aggiornamento di Alembic?


95

Devo modificare i dati durante un aggiornamento di Alembic.

Al momento ho un tavolo "giocatori" in una prima revisione:

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

Voglio introdurre un tavolo "squadre". Ho creato una seconda revisione:

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

Vorrei che la seconda migrazione aggiungesse anche i seguenti dati:

  1. Popolare la tabella delle squadre:

    INSERT INTO teams (name) SELECT DISTINCT team FROM players;
  2. Aggiorna players.team_id in base al nome players.team:

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

Come si eseguono inserimenti e aggiornamenti all'interno dello script di aggiornamento?

Risposte:


147

Quello che stai chiedendo è una migrazione dei dati , al contrario della migrazione dello schema che è più diffusa nei documenti di Alembic.

Questa risposta presuppone che tu stia usando dichiarativo (al contrario di class-Mapper-Table o core) per definire i tuoi modelli. Dovrebbe essere relativamente semplice adattarlo alle altre forme.

Nota che Alembic fornisce alcune funzioni di dati di base: op.bulk_insert()e op.execute(). Se le operazioni sono abbastanza minime, usa quelle. Se la migrazione richiede relazioni o altre interazioni complesse, preferisco utilizzare tutta la potenza dei modelli e delle sessioni come descritto di seguito.

Di seguito è riportato uno script di migrazione di esempio che configura alcuni modelli dichiarativi che verranno utilizzati per manipolare i dati in una sessione. I punti chiave sono:

  1. Definisci i modelli di base di cui hai bisogno, con le colonne di cui avrai bisogno. Non hai bisogno di tutte le colonne, solo della chiave primaria e di quelle che utilizzerai.

  2. All'interno della funzione di aggiornamento, utilizzare op.get_bind()per ottenere la connessione corrente e fare una sessione con essa.

    • Oppure utilizzare bind.execute()per utilizzare il livello inferiore di SQLAlchemy per scrivere direttamente query SQL. Questo è utile per semplici migrazioni.
  3. Usa i modelli e la sessione come faresti normalmente nella tua applicazione.

"""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 migrazione definisce modelli separati perché i modelli nel codice rappresentano lo stato corrente del database, mentre le migrazioni rappresentano passaggi lungo il percorso . Il tuo database potrebbe trovarsi in qualsiasi stato lungo quel percorso, quindi i modelli potrebbero non essere ancora sincronizzati con il database. A meno che tu non stia molto attento, l'utilizzo diretto dei modelli reali causerà problemi con colonne mancanti, dati non validi, ecc. È più chiaro indicare in modo esplicito esattamente quali colonne e modelli utilizzerai nella migrazione.


11

È inoltre possibile utilizzare l'SQL diretto, vedere ( Alembic Operation Reference ) come nell'esempio seguente:

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 ###

Nel caso in cui volessi sempre leggere un'istruzione SQL da un file esterno e poi passarla a op.executein upgrade(), c'è un modo per fornire un modello predefinito da utilizzare tramite alembic revisioncomando (un corpo predefinito per il .pyfile generato )?
Quentin

1
Non so @Quentin. È un'idea interessante.
Martlark

6

Consiglio di utilizzare le istruzioni di base di SQLAlchemy utilizzando una tabella ad-hoc, come dettagliato nella documentazione ufficiale , perché consente l'uso dell'SQL agnostico e della scrittura pitonica ed è anche autonomo. SQLAlchemy Core è il meglio di entrambi i mondi per gli script di migrazione.

Ecco un esempio del concetto:

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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.