web-dev-qa-db-ja.com

Alembicの移行中に列のコンテンツを更新する

私のdbモデルにオブジェクトUserが含まれているとします。

_Base = declarative_base() 

class User(Base):                                                               
    __tablename__ = 'users'                                                     

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

私のデータベースには、n行のusersテーブルが含まれています。ある時点で、私はnamefirstnamelastnameに分割することを決定し、そして _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')
_

この問題には、多かれ少なかれハックな解決策があるようです。 これこれ はどちらも使用を提案します execute()bulk_insert() 移行中に生のSQLステートメントを実行します。 この(不完全な)ソリューション 現在のdbモデルをインポートしますが、そのモデルが変更されると、そのアプローチは壊れやすくなります。

Alembicの移行中に列データの既存のコンテンツを移行および変更するにはどうすればよいですか?推奨される方法は何ですか、それはどこに文書化されていますか?

16
Jens

norbertpy’s answer で提案されたソリューションは最初はいいように聞こえますが、根本的な欠陥が1つあると思います。複数のトランザクションが導入され、ステップ間でDBがファンキーで一貫性のない状態になります。また、ツールがDBのデータなしでDBのスキーマを移行することも奇妙に思えます( 私のコメント を参照)。 2つは緊密に結合されているため、それらを分離できません。

いくつかの会話といくつかの会話の後( this Gist のコードスニペットを参照)、次の解決策を決定しました。

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

このソリューションに関する2つのコメント:

  1. 参照されているGistに記載されているように、Alembicの新しいバージョンでは表記が少し異なります。
  2. DBドライバーによっては、コードの動作が異なる場合があります。どうやら、MySQLは上記のコードを単一のトランザクションとして処理しない「暗黙のコミットを引き起こすステートメント」 を参照)。したがって、DBの実装を確認する必要があります。

downgrade()関数も同様に実装できます。

補遺。スキーマの移行とデータの移行のペアの例については、Alembicクックブックの 条件付き移行要素 セクションを参照してください。

14
Jens

alembicはスキーマ移行ツールであり、データ移行ではありません。そのように使用することもできますが。そのため、多くのドキュメントが見つかりません。とはいえ、3つの個別のリビジョンを作成することになります。

  1. firstnameを削除せずにlastnamenameを追加する
  2. アプリと同じようにすべてのユーザーを読み取り、名前を分割してから、firstlastを更新します。例えば.

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

4
norbertpy