Como executo inserções e atualizações em um script de atualização do Alembic?

94

Preciso alterar os dados durante uma atualização do Alembic.

Atualmente, tenho uma mesa de 'jogadores' em uma primeira revisão:

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

Eu quero apresentar uma tabela de 'times'. Eu criei uma segunda revisão:

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

Eu gostaria que a segunda migração também adicionasse os seguintes dados:

  1. Preencher a tabela de equipes:

    INSERT INTO teams (name) SELECT DISTINCT team FROM players;
  2. Atualize players.team_id com base em players.team name:

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

Como executo inserções e atualizações dentro do script de atualização?

Arek S
fonte

Respostas:

145

O que você está pedindo é uma migração de dados , em oposição à migração de esquema que é mais comum nos documentos do Alembic.

Esta resposta assume que você está usando declarativo (ao contrário de class-Mapper-Table ou core) para definir seus modelos. Deve ser relativamente simples adaptar isso às outras formas.

Observe que o Alembic fornece algumas funções básicas de dados: op.bulk_insert()e op.execute(). Se as operações forem mínimas, use-as. Se a migração exigir relacionamentos ou outras interações complexas, prefiro usar todo o poder dos modelos e sessões conforme descrito abaixo.

A seguir está um script de migração de exemplo que configura alguns modelos declarativos que serão usados ​​para manipular dados em uma sessão. Os pontos principais são:

  1. Defina os modelos básicos de que você precisa, com as colunas de que precisa. Você não precisa de todas as colunas, apenas da chave primária e das que usará.

  2. Na função de atualização, use op.get_bind()para obter a conexão atual e faça uma sessão com ela.

    • Ou use bind.execute()para usar o nível inferior do SQLAlchemy para escrever consultas SQL diretamente. Isso é útil para migrações simples.
  3. Use os modelos e a sessão como faria normalmente em seu aplicativo.

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

A migração define modelos separados porque os modelos em seu código representam o estado atual do banco de dados, enquanto as migrações representam etapas ao longo do caminho . Seu banco de dados pode estar em qualquer estado ao longo desse caminho, então os modelos podem não sincronizar com o banco de dados ainda. A menos que você seja muito cuidadoso, usar os modelos reais diretamente causará problemas com colunas ausentes, dados inválidos, etc. É mais claro declarar explicitamente quais colunas e modelos você usará na migração.

davidismo
fonte
11

Você também pode usar o SQL direto, consulte ( Referência de operação do Alembic ) como no exemplo a seguir:

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
fonte
Caso eu sempre quisesse ler uma instrução SQL de um arquivo externo e depois passá-la para op.executedentro upgrade(), há uma maneira de fornecer um modelo padrão para ser usado por alembic revisioncomando (um corpo padrão para o .pyarquivo gerado )?
Quentin
1
Não sei @Quentin. É uma ideia interessante.
Martlark
6

Eu recomendo o uso de instruções centrais SQLAlchemy usando uma tabela ad-hoc, conforme detalhado na documentação oficial , porque permite o uso de SQL agnóstico e escrita pythônica e também é independente. SQLAlchemy Core é o melhor dos dois mundos para scripts de migração.

Aqui está um exemplo do conceito:

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
fonte