Wie führe ich Einfügungen und Aktualisierungen in einem Alembic-Upgrade-Skript aus?

94

Ich muss Daten während eines Alembic-Upgrades ändern.

Ich habe derzeit einen "Spielertisch" in einer ersten Überarbeitung:

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

Ich möchte einen "Team" -Tisch vorstellen. Ich habe eine zweite Revision erstellt:

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

Ich möchte, dass bei der zweiten Migration auch die folgenden Daten hinzugefügt werden:

  1. Teamtabelle füllen:

    INSERT INTO teams (name) SELECT DISTINCT team FROM players;
  2. Aktualisieren Sie die Datei player.team_id basierend auf dem Namen des Spielerteams:

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

Wie führe ich Einfügungen und Aktualisierungen im Upgrade-Skript aus?

Arek S.
quelle

Antworten:

145

Was Sie verlangen, ist eine Datenmigration im Gegensatz zur Schemamigration , die in den Alembic-Dokumenten am häufigsten verwendet wird.

Bei dieser Antwort wird davon ausgegangen, dass Sie deklarativ (im Gegensatz zu Class-Mapper-Table oder Core) verwenden, um Ihre Modelle zu definieren. Es sollte relativ einfach sein, dies an die anderen Formen anzupassen.

Beachten Sie, dass Alembic einige grundlegende Datenfunktionen bietet: op.bulk_insert()undop.execute() . Wenn die Operationen ziemlich minimal sind, verwenden Sie diese. Wenn die Migration Beziehungen oder andere komplexe Interaktionen erfordert, bevorzuge ich die Nutzung der vollen Leistung von Modellen und Sitzungen, wie unten beschrieben.

Im Folgenden finden Sie ein Beispiel für ein Migrationsskript, mit dem einige deklarative Modelle eingerichtet werden, mit denen Daten in einer Sitzung bearbeitet werden. Die wichtigsten Punkte sind:

  1. Definieren Sie die Grundmodelle, die Sie benötigen, mit den Spalten, die Sie benötigen. Sie benötigen nicht jede Spalte, nur den Primärschlüssel und die von Ihnen verwendeten.

  2. Verwenden Sie op.get_bind()in der Upgrade-Funktion, um die aktuelle Verbindung abzurufen und eine Sitzung damit zu erstellen.

    • Oder verwenden Sie bind.execute(), um die untere Ebene von SQLAlchemy zu verwenden, um SQL-Abfragen direkt zu schreiben. Dies ist nützlich für einfache Migrationen.
  3. Verwenden Sie die Modelle und die Sitzung wie gewohnt in Ihrer Anwendung.

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

Die Migration definiert separate Modelle, da die Modelle in Ihrem Code den aktuellen Status der Datenbank darstellen, während die Migrationen Schritte auf dem Weg darstellen . Ihre Datenbank befindet sich möglicherweise in einem beliebigen Zustand auf diesem Pfad, sodass die Modelle möglicherweise noch nicht mit der Datenbank synchronisiert sind. Wenn Sie nicht sehr vorsichtig sind, führt die direkte Verwendung der realen Modelle zu Problemen mit fehlenden Spalten, ungültigen Daten usw. Es ist klarer, genau anzugeben, welche Spalten und Modelle Sie bei der Migration verwenden werden.

Davidismus
quelle
11

Sie können auch Direct SQL siehe ( Alembic Operation Reference ) wie im folgenden Beispiel verwenden:

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
quelle
Wenn ich schon immer eine SQL-Anweisung aus einer externen Datei lesen und dann an op.executein übergeben wollte upgrade(), gibt es eine Möglichkeit, eine Standardvorlage bereitzustellen, die vom alembic revisionBefehl verwendet werden kann (ein Standardtext für die generierte .pyDatei)?
Quentin
1
Ich kenne @Quentin nicht. Das ist eine interessante Idee.
Martlark
6

Ich empfehle die Verwendung von SQLAlchemy-Kernanweisungen mithilfe einer Ad-hoc-Tabelle, wie in der offiziellen Dokumentation beschrieben , da diese die Verwendung von agnostischem SQL und pythonischem Schreiben ermöglicht und auch in sich geschlossen ist. SQLAlchemy Core ist das Beste aus beiden Welten für Migrationsskripte.

Hier ist ein Beispiel für das Konzept:

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
quelle