¿Cómo ejecuto inserciones y actualizaciones en un script de actualización de Alembic?

95

Necesito alterar los datos durante una actualización de Alembic.

Actualmente tengo una tabla de 'jugadores' en una primera revisión:

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

Quiero presentar una tabla de 'equipos'. He creado una segunda revisión:

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

Me gustaría que la segunda migración también agregue los siguientes datos:

  1. Completar la tabla de equipos:

    INSERT INTO teams (name) SELECT DISTINCT team FROM players;
  2. Actualiza players.team_id según el nombre de players.team:

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

¿Cómo ejecuto inserciones y actualizaciones dentro del script de actualización?

Arek S
fuente

Respuestas:

147

Lo que está pidiendo es una migración de datos , a diferencia de la migración de esquema que es más frecuente en los documentos de Alembic.

Esta respuesta asume que está utilizando declarativo (a diferencia de class-Mapper-Table o core) para definir sus modelos. Debería ser relativamente sencillo adaptar esto a las otras formas.

Tenga en cuenta que Alembic proporciona algunas funciones de datos básicas: op.bulk_insert()y op.execute(). Si las operaciones son bastante mínimas, utilícelas. Si la migración requiere relaciones u otras interacciones complejas, prefiero usar todo el poder de los modelos y sesiones como se describe a continuación.

A continuación, se muestra un script de migración de ejemplo que configura algunos modelos declarativos que se utilizarán para manipular datos en una sesión. Los puntos clave son:

  1. Defina los modelos básicos que necesita, con las columnas que necesitará. No necesita todas las columnas, solo la clave principal y las que utilizará.

  2. Dentro de la función de actualización, use op.get_bind()para obtener la conexión actual y hacer una sesión con ella.

    • O use bind.execute()para usar el nivel inferior de SQLAlchemy para escribir consultas SQL directamente. Esto es útil para migraciones simples.
  3. Utilice los modelos y la sesión como lo haría normalmente en su aplicación.

"""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 migración define modelos separados porque los modelos en su código representan el estado actual de la base de datos, mientras que las migraciones representan pasos en el camino . Su base de datos puede estar en cualquier estado a lo largo de esa ruta, por lo que es posible que los modelos aún no se sincronicen con la base de datos. A menos que tenga mucho cuidado, usar los modelos reales directamente causará problemas con columnas faltantes, datos no válidos, etc. Es más claro indicar explícitamente exactamente qué columnas y modelos utilizará en la migración.

davidismo
fuente
11

También puede usar SQL directo, consulte ( Referencia de operación de alambique ) como en el siguiente ejemplo:

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
fuente
En caso de que siempre he querido leer una instrucción SQL de un archivo externo y luego pasarlo a op.executeen upgrade(), ¿hay una manera de proporcionar una plantilla predeterminada para ser utilizado por alembic revisioncomandos (un cuerpo predeterminado para la generada .pyarchivo)?
Quentin
1
No lo sé @Quentin. Es una idea interesante.
Martlark
6

Recomiendo usar las declaraciones centrales de SQLAlchemy usando una tabla ad-hoc, como se detalla en la documentación oficial , porque permite el uso de SQL agnóstico y escritura pitónica y también es autónomo. SQLAlchemy Core es lo mejor de ambos mundos para los scripts de migración.

A continuación se muestra un ejemplo del concepto:

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
fuente