web-dev-qa-db-fra.com

Mettre à jour le contenu des colonnes lors de la migration d'Alembic

Supposons que mon modèle db contienne un objet User:

Base = declarative_base() 

class User(Base):                                                               
    __tablename__ = 'users'                                                     

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

et ma base de données contient une table users avec n lignes. À un moment donné, je décide de diviser le name en firstname et lastname, et pendant alembic upgrade head je voudrais que mes données être migré également.

La migration auto-générée Alembic est la suivante:

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

Il semble y avoir plusieurs solutions plus ou moins hacky à ce problème. Celui-ci et celui-ci proposent tous deux d'utiliser execute() et bulk_insert() pour exécuter des instructions SQL brutes lors d'une migration. Cette solution (incomplète) importe le modèle db actuel mais cette approche est fragile lorsque ce modèle change.

Comment migrer et modifier le contenu existant des données de colonne lors d'une migration Alembic? Quelle est la méthode recommandée et où est-elle documentée?

16
Jens

La solution proposée dans réponse de norbertpy sonne bien au début, mais je pense qu'elle a un défaut fondamental: elle introduirait plusieurs transactions - entre les étapes, la base de données serait dans un état génial et incohérent. Il me semble également étrange (voir mon commentaire ) qu'un outil migrerait le schéma d'une base de données sans les données de la base de données; les deux sont trop étroitement liés pour les séparer.

Après quelques fouilles et plusieurs conversations (voir les extraits de code dans ce Gist ), j'ai décidé de la solution suivante:

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

Deux commentaires sur cette solution:

  1. Comme indiqué dans le Gist référencé, les nouvelles versions d'Alembic ont une notation légèrement différente.
  2. Selon le pilote DB, le code peut se comporter différemment. Apparemment, MySQL ne pas gère le code ci-dessus comme une transaction unique (voir "Déclarations qui provoquent une validation implicite" ). Vous devez donc vérifier avec votre implémentation DB.

La fonction downgrade() peut être implémentée de la même manière.

Addendum. Voir la section Éléments de migration conditionnelle dans le livre de recettes Alembic pour des exemples de couplage de la migration de schéma avec la migration de données.

14
Jens

alembic est un outil de migration de schéma et non une migration de données. Bien qu'il puisse également être utilisé de cette façon. C'est pourquoi vous ne trouverez pas beaucoup de documents à ce sujet. Cela dit, j'aurais créé trois révisions distinctes:

  1. ajouter firstname et lastname sans supprimer le name
  2. lisez tous vos utilisateurs comme vous le feriez dans votre application et divisez leur nom, puis mettez à jour first et last. par exemple.

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

4
norbertpy