ฉันจะดำเนินการแทรกและอัพเดตในสคริปต์อัปเกรด Alembic ได้อย่างไร


102

ฉันต้องการแก้ไขข้อมูลระหว่างการอัพเกรด Alembic

ขณะนี้ฉันมีตาราง 'ผู้เล่น' ในการแก้ไขครั้งแรก:

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

ฉันต้องการแนะนำตาราง 'ทีม' ฉันได้สร้างการแก้ไขครั้งที่สอง:

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

ฉันต้องการให้การย้ายข้อมูลครั้งที่สองเพิ่มข้อมูลต่อไปนี้ด้วย:

  1. เติมตารางทีม:

    INSERT INTO teams (name) SELECT DISTINCT team FROM players;
    
  2. อัปเดต player.team_id ตามชื่อผู้เล่นทีม:

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

ฉันจะดำเนินการแทรกและอัพเดตภายในสคริปต์อัพเกรดได้อย่างไร?

คำตอบ:


158

สิ่งที่คุณขอคือการย้ายข้อมูลซึ่งตรงข้ามกับการย้ายสคีมาที่แพร่หลายมากที่สุดในเอกสารของ Alembic

คำตอบนี้ถือว่าคุณใช้ declarative (ซึ่งตรงข้ามกับ class-Mapper-Table หรือ core) เพื่อกำหนดโมเดลของคุณ ควรปรับให้เข้ากับรูปแบบอื่น ๆ ค่อนข้างตรงไปตรงมา

โปรดทราบว่า Alembic มีฟังก์ชันข้อมูลพื้นฐานบางอย่าง: op.bulk_insert()และop.execute(). หากการดำเนินการค่อนข้างน้อยให้ใช้สิ่งเหล่านี้ หากการย้ายข้อมูลต้องใช้ความสัมพันธ์หรือการโต้ตอบที่ซับซ้อนอื่น ๆ ฉันต้องการใช้โมเดลและเซสชันอย่างเต็มประสิทธิภาพตามที่อธิบายไว้ด้านล่าง

ต่อไปนี้เป็นตัวอย่างสคริปต์การย้ายข้อมูลที่ตั้งค่าโมเดลที่ประกาศซึ่งจะใช้ในการจัดการข้อมูลในเซสชัน ประเด็นสำคัญคือ:

  1. กำหนดโมเดลพื้นฐานที่คุณต้องการพร้อมคอลัมน์ที่คุณต้องการ คุณไม่จำเป็นต้องมีทุกคอลัมน์มีเพียงคีย์หลักและคอลัมน์ที่คุณจะใช้

  2. ภายในฟังก์ชันอัพเกรดใช้op.get_bind()เพื่อรับการเชื่อมต่อปัจจุบันและสร้างเซสชันด้วย

    • หรือใช้bind.execute()เพื่อใช้ระดับล่างของ SQLAlchemy เพื่อเขียนคำสั่ง SQL โดยตรง สิ่งนี้มีประโยชน์สำหรับการย้ายข้อมูลอย่างง่าย
  3. ใช้โมเดลและเซสชันตามปกติในแอปพลิเคชันของคุณ

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

ข้อกำหนดเกี่ยวการโยกย้ายแยกรุ่นเพราะรุ่นในรหัสของคุณเป็นตัวแทนของรัฐในปัจจุบันของฐานข้อมูลในขณะที่การโยกย้ายแทนขั้นตอนไปพร้อมกัน ฐานข้อมูลของคุณอาจอยู่ในสถานะใด ๆ ตามเส้นทางนั้นดังนั้นโมเดลอาจยังไม่ได้ซิงค์กับฐานข้อมูล หากคุณไม่ระมัดระวังการใช้แบบจำลองจริงโดยตรงจะทำให้เกิดปัญหากับคอลัมน์ที่หายไปข้อมูลที่ไม่ถูกต้อง ฯลฯ การระบุอย่างชัดเจนว่าคุณจะใช้คอลัมน์และโมเดลใดในการย้ายข้อมูล


12

คุณยังสามารถใช้ Direct SQL ดู ( Alembic Operation Reference ) ดังตัวอย่างต่อไปนี้:

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

ในกรณีที่ฉันอยากจะอ่านคำสั่ง SQL จากแฟ้มภายนอกและจากนั้นผ่านไปop.executeในupgrade()จะมีวิธีที่จะให้แม่แบบเริ่มต้นที่จะใช้โดยalembic revisionคำสั่ง (ร่างกายเริ่มต้นสำหรับการสร้าง.pyไฟล์)?
Quentin

1
ไม่รู้จัก @Quentin. เป็นความคิดที่น่าสนใจ
Martlark

7

ฉันขอแนะนำให้ใช้คำสั่งหลัก SQLAlchemy โดยใช้ตารางเฉพาะกิจตามรายละเอียดในเอกสารอย่างเป็นทางการเนื่องจากอนุญาตให้ใช้ SQL ที่ไม่เชื่อเรื่องพระเจ้าและการเขียนแบบไพโธนิกและยังมีอยู่ในตัวด้วย SQLAlchemy Core เป็นสิ่งที่ดีที่สุดในทั้งสองโลกสำหรับสคริปต์การย้ายข้อมูล

นี่คือตัวอย่างของแนวคิด:

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
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.