【问题标题】:Update column content during Alembic migration在 Alembic 迁移期间更新列内容
【发布时间】:2017-08-26 11:21:12
【问题描述】:

假设我的数据库模型包含一个对象User:

Base = declarative_base() 

class User(Base):                                                               
    __tablename__ = 'users'                                                     

    id = Column(String(32), primary_key=True, default=...) 
    name = Column(Unicode(100))                                             

我的数据库包含一个带有 n 行的 users 表。在某些时候,我决定将name 拆分为firstname 和lastname,并在alembic upgrade head 期间也希望迁移我的数据。

自动生成的Alembic迁移如下:

def upgrade():
    op.add_column('users', sa.Column('lastname', sa.Unicode(length=50), nullable=True))
    op.add_column('users', sa.Column('firstname', sa.Unicode(length=50), nullable=True))

    # Assuming that the two new columns have been committed and exist at
    # this point, I would like to iterate over all rows of the name column,
    # split the string, write it into the new firstname and lastname rows,
    # and once that has completed, continue to delete the name column.

    op.drop_column('users', 'name')                                             

def downgrade():
    op.add_column('users', sa.Column('name', sa.Unicode(length=100), nullable=True))

    # Do the reverse of the above.

    op.drop_column('users', 'firstname')                                        
    op.drop_column('users', 'lastname')

对于这个问题,似乎有多种或多或少的 hacky 解决方案。 This one 和 this one 都建议使用 execute() 和 bulk_insert() 在迁移期间执行原始 SQL 语句。 This (incomplete) solution 导入当前的数据库模型,但是当模型发生变化时,这种方法很脆弱。

如何在 Alembic 迁移期间迁移和修改列数据的现有内容?推荐的方法是什么,它记录在哪里?

【问题讨论】:

    标签: python-3.x alembic sqlalchemy-migrate


    【解决方案1】:

    alembic 是架构迁移工具,而不是数据迁移。虽然它也可以这样使用。这就是为什么你不会找到很多关于它的文档的原因。也就是说,我会创建三个单独的修订:

    1. 添加firstname和lastname而不删除name
    2. 像在应用程序中一样读取所有用户并拆分他们的名称,然后更新first 和last。例如

      for user in session.query(User).all():
          user.firstname, user.lastname = user.name.split(' ')
      session.commit()
      
    3. 删除name

    【讨论】:

    • 我认为不能将模式迁移与数据迁移分开。两者结合在一起,一个人不能迁移数据库的模式而不沿着它的数据迁移。那么,如果 Alembic 被设计成只做一个而几乎不做另一个,那它有什么用呢?
    • 不推荐这种方法。如果 User 的架构发生变化,那么您的迁移将开始失败。
    【解决方案2】:

    norbertpy’s answer 中提出的解决方案一开始听起来不错,但我认为它有一个根本缺陷:它会引入多个事务——在这些步骤之间,数据库将处于时髦、不一致的状态。我也觉得奇怪(见my comment),一个工具会在没有数据库数据的情况下迁移数据库的模式;两者联系得太紧密,无法分开。

    经过一番摸索和几次对话(参见this Gist 中的代码 sn-ps),我决定采用以下解决方案:

    def upgrade():
    
        # Schema migration: add all the new columns.
        op.add_column('users', sa.Column('lastname', sa.Unicode(length=50), nullable=True))
        op.add_column('users', sa.Column('firstname', sa.Unicode(length=50), nullable=True))
    
        # Data migration: takes a few steps...
        # Declare ORM table views. Note that the view contains old and new columns!        
        t_users = sa.Table(
            'users',
            sa.MetaData(),
            sa.Column('id', sa.String(32)),
            sa.Column('name', sa.Unicode(length=100)), # Old column.
            sa.Column('lastname', sa.Unicode(length=50)), # Two new columns.
            sa.Column('firstname', sa.Unicode(length=50)),
            )
        # Use Alchemy's connection and transaction to noodle over the data.
        connection = op.get_bind()
        # Select all existing names that need migrating.
        results = connection.execute(sa.select([
            t_users.c.id,
            t_users.c.name,
            ])).fetchall()
        # Iterate over all selected data tuples.
        for id_, name in results:
            # Split the existing name into first and last.
            firstname, lastname = name.rsplit(' ', 1)
            # Update the new columns.
            connection.execute(t_users.update().where(t_users.c.id == id_).values(
                lastname=lastname,
                firstname=firstname,
                ))
    
        # Schema migration: drop the old column.
        op.drop_column('users', 'name')                                             
    

    关于这个解决方案的两个方面:

    1. 如引用的 Gist 中所述,较新版本的 Alembic 符号略有不同。
    2. 根据 DB 驱动程序,代码的行为可能会有所不同。显然,MySQL 不 将上述代码作为单个事务处理(请参阅“Statements That Cause an Implicit Commit”)。因此,您必须检查您的数据库实施。

    downgrade() 函数可以类似地实现。

    附录。有关架构迁移与数据迁移配对的示例,请参阅 Alembic Cookbook 中的 Conditional Migration Elements 部分。

    【讨论】:

    • 这是一个很好的答案——但我不得不对你的代码做一点改动。必须通过 t_users.c.id 或 t_users.c.name 在两个 connect.execute 调用中指定列。您能否确认这一点,并可能编辑您的答案(或解释发生了什么,如果您碰巧知道的话)?
    • @daveruinseverything,确认并修复了示例代码;谢谢!
    • 不幸的是,这个答案实际上是一开始听起来不错但有一个根本缺陷的答案。至少在一个永远在线的 Web 应用程序的上下文中,这不是一个很好的方法。原因是在大表上数据迁移可能非常昂贵,因此您通常希望避免在部署应用程序的“热路径”中执行此操作。如果您的数据库已经有一些负载,这可能会增加它并真正减慢您的应用程序,因此能够在后台运行它很好,也许会限制写入速度。
    • ^ 补充一下,因为我超出了限制-@norbertby 的答案是更好的方法的最后一个原因是,您可以在做任何不可逆转的事情之前停下来。这是一个非常简单的示例,但一般而言,您的数据迁移可能在测试数据上运行良好,但在生产中由于数据种类更广泛,它会破坏某些东西。如果您将数据保留在“名称”和名字和姓氏列中,那么如果出现任何问题,您可以回滚您的应用程序。如果你已经不可逆地迁移它,你的状态会更糟。
    • @danny,这就是为什么我在迁移之前 对数据库进行备份,以及为什么我在实时尝试冷火鸡之前在该备份上测试迁移分贝。
    猜你喜欢
    • 2023-03-31
    • 2014-08-09
    • 2017-09-07
    • 2021-12-17
    • 2013-07-04
    • 2019-05-02
    • 2014-05-18
    • 2016-09-06
    • 2013-01-16
    相关资源
    最近更新 更多