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?
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:
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.
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:
firstname
et lastname
sans supprimer le name
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()
supprimer name