python - migrations - migrate force django
¿Cuál es el mejor enfoque para cambiar las claves primarias en una aplicación Django existente? (6)
Tengo una aplicación que está en modo BETA. El modelo de esta aplicación tiene algunas clases con una clave primaria explícita. Como consecuencia, Django usa los campos y no crea una identificación automáticamente.
class Something(models.Model):
name = models.CharField(max_length=64, primary_key=True)
Creo que fue una mala idea (vea el error de Unicode al guardar un objeto en el administrador de django ) y me gustaría retroceder y tener una identificación para cada clase de mi modelo.
class Something(models.Model):
name = models.CharField(max_length=64, db_index=True)
He realizado los cambios en mi modelo (reemplazar todas las teclas primary_key = True por db_index = True) y deseo migrar la base de datos a south .
Desafortunadamente, la migración falla con el siguiente mensaje: ValueError: You cannot add a null=False column without a default value.
Estoy evaluando las diferentes soluciones para este problema. ¿Alguna sugerencia?
Gracias por tu ayuda
Actualmente está fallando porque está agregando una columna pk que rompe los requisitos NOT NULL y UNIQUE.
Debe dividir la migración en varios pasos , separando las migraciones de esquema y las migraciones de datos:
- agregue la nueva columna, indexada pero no clave principal, con un valor predeterminado (migración ddl)
- migrar los datos: rellene la nueva columna con el valor correcto (migración de datos)
- marque la nueva clave principal de la columna y elimine la columna pk anterior si se ha vuelto innecesaria (migración ddl)
De acuerdo, tu modelo probablemente esté equivocado.
La clave primaria formal siempre debe ser una clave sustituta. Nunca nada más. [Palabras fuertes. Diseñador de bases de datos desde los años 80. La lección importante que aprendimos es esto: todo es cambiante, incluso cuando los usuarios juran en las tumbas de sus madres que el valor no puede cambiarse, es realmente una clave natural que puede tomarse como principal. No es primaria. Sólo los sustitutos pueden ser primarios.]
Estás haciendo una cirugía a corazón abierto. No ensucie con la migración del esquema. Estás reemplazando el esquema.
Descargue sus datos en archivos JSON. Use las propias herramientas django-admin.py internas de Django para esto. Debe crear un archivo de descarga para cada uno que cambiará y cada tabla que depende de una clave que se está creando. Archivos separados hacen esto un poco más fácil de hacer.
Descarte las tablas que va a cambiar del esquema anterior.
Las tablas que dependen de estas tablas tendrán sus FK cambiados; puede actualizar las filas en su lugar o, quizás sea más sencillo, eliminar y volver a insertar estas filas, también.
Crea el nuevo esquema. Esto solo creará las tablas que están cambiando.
Escriba scripts para leer y volver a cargar los datos con las nuevas claves. Estos son cortos y muy similares. Cada script usará
json.load()
para leer objetos del archivo fuente; a continuación, creará sus objetos de esquema a partir de los objetos de línea de tupla JSON que se crearon para usted. A continuación, puede insertarlas en la base de datos.Tienes dos casos.
Las tablas con el cambio de PK cambiado se insertarán y obtendrán nuevas PK. Estos deben estar "conectados en cascada" a otras tablas para asegurar que los FK de la otra tabla también se modifiquen.
Las tablas con FK que cambian deberán ubicar la fila en la tabla externa y actualizar su referencia de FK.
Alternativa.
Renombra todas tus tablas antiguas.
Crea el nuevo esquema completo.
Escriba SQL para migrar todos los datos del esquema antiguo al esquema nuevo. Esto tendrá que reasignar ingeniosamente las teclas a medida que avanza.
Suelte las tablas antiguas renombradas.
Logré hacer esto con las migraciones de django 1.10.4 y mysql 5.5, pero no fue fácil.
Tuve una clave primaria varchar con varias claves externas. Agregué un campo de id
, datos migrados y claves externas. Así es como:
- Adición de campo de clave principal futuro. Agregué un campo
id = models.IntegerField(default=0)
a mi modelo principal y generé una migración automática. Migración de datos simple para generar nuevas claves primarias:
def fill_ids(apps, schema_editor): Model = apps.get_model(''<module>'', ''<model>'') for id, code in enumerate(Model.objects.all()): code.id = id + 1 code.save() class Migration(migrations.Migration): dependencies = […] operations = [migrations.RunPython(fill_ids)]
Migración de claves externas existentes. Escribí una migración combinada:
def change_model_fks(apps, schema_editor): Model = apps.get_model(''<module>'', ''<model>'') # Our model we want to change primary key for FkModel = apps.get_model(''<module>'', ''<fk_model>'') # Other model that references first one via foreign key mapping = {} for model in Model.objects.all(): mapping[model.old_pk_field] = model.id # map old primary keys to new for fk_model in FkModel.objects.all(): if fk_model.model_id: fk_model.model_id = mapping[fk_model.model_id] # change the reference fk_model.save() class Migration(migrations.Migration): dependencies = […] operations = [ # drop foreign key constraint migrations.AlterField( model_name=''<FkModel>'', name=''model'', field=models.ForeignKey(''<Model>'', blank=True, null=True, db_constraint=False) ), # change references migrations.RunPython(change_model_fks), # change field from varchar to integer, drop index migrations.AlterField( model_name=''<FkModel>'', name=''model'', field=models.IntegerField(''<Model>'', blank=True, null=True) ), ]
Intercambio de claves primarias y restauración de claves foráneas. De nuevo, una migración personalizada.
primary_key=True
automáticamente la base para esta migración cuando a)primary_key=True
de la clave principal anterior yb)primary_key=True
campo deid
class Migration(migrations.Migration): dependencies = […] operations = [ # Drop old primary key migrations.AlterField( model_name=''<Model>'', name=''<old_pk_field>'', field=models.CharField(max_length=100), ), # Create new primary key migrations.RunSQL( [''ALTER TABLE <table> CHANGE id id INT (11) NOT NULL PRIMARY KEY AUTO_INCREMENT''], [''ALTER TABLE <table> CHANGE id id INT (11) NULL'', ''ALTER TABLE <table> DROP PRIMARY KEY''], state_operations=[migrations.AlterField( model_name=''<Model>'', name=''id'', field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name=''ID''), )] ), # Recreate foreign key constraints migrations.AlterField( model_name=''<FkModel>'', name=''model'', field=models.ForeignKey(blank=True, null=True, to=''<module>.<Model>''), ]
Para cambiar la clave principal con south puede usar el comando south.db.create_primary_key en datamigration. Para cambiar tu CharField pk personalizado a AutoField estándar debes hacer:
1) crea nuevo campo en tu modelo
class MyModel(Model):
id = models.AutoField(null=True)
1.1) si tiene una clave foránea en algún otro modelo para este modelo, cree un nuevo campo de fk falso en este modelo también (use IntegerField, luego se convertirá)
class MyRelatedModel(Model):
fake_fk = models.IntegerField(null=True)
2) crear la migración sur automática y migrar:
./manage.py schemamigration --auto
./manage.py migrate
3) crear nueva datamigration
./manage.py datamigration <your_appname> fill_id
en esta fecha, llene estos nuevos campos de identificación y fk con números (solo enumérelos)
for n, obj in enumerate(orm.MyModel.objects.all()):
obj.id = n
# update objects with foreign keys
obj.myrelatedmodel_set.all().update(fake_fk = n)
obj.save()
db.delete_primary_key(''my_app_mymodel'')
db.create_primary_key(''my_app_mymodel'', [''id''])
4) en sus modelos set primary_key = True en su nuevo campo pk
id = models.AutoField(primary_key=True)
5) eliminar el campo de la clave principal anterior (si no es necesario) crear la migración automática y migrar.
5.1) si tiene claves foráneas, elimine también los campos de claves foráneas antiguas (migrar)
6) Último paso - restaurar las relaciones clave de Fireign. Cree de nuevo el campo fk real, y elimine su campo fake_fk, cree la migración automática PERO NO MIGRE (!) - necesita modificar la migración automática creada: en lugar de crear una nueva fk y eliminar fake_fk - renombrar la columna fake_fk
# in your models
class MyRelatedModel(Model):
# delete fake_fk
# fake_fk = models.InegerField(null=True)
# create real fk
mymodel = models.FoeignKey(''MyModel'', null=True)
# in migration
def forwards(self, orm):
# left this without change - create fk field
db.add_column(''my_app_myrelatedmodel'', ''mymodel'',
self.gf(''django.db.models.fields.related.ForeignKey'')(default=1, related_name=''lots'', to=orm[''my_app.MyModel'']),keep_default=False)
# remove fk column and rename fake_fk
db.delete_column(''my_app_myrelatedmodel'', ''mymodel_id'')
db.rename_column(''my_app_myrelatedmodel'', ''fake_fk'', ''mymodel_id'')
así que fake_fk previamente llenado se convierte en una columna, que contiene datos de relaciones reales, y no se pierde después de todos los pasos anteriores.
Tuve el mismo problema hoy y llegué a una solución inspirada en las respuestas anteriores.
Mi modelo tiene una tabla de "Ubicación". Tiene un CharField llamado "unique_id" y lo hice tontamente como clave principal, el año pasado. Por supuesto, no resultaron ser tan únicos como se esperaba en ese momento. También hay un modelo de "Medición programada" que tiene una clave externa a "Ubicación".
Ahora quiero corregir ese error y darle a Location una clave principal ordinaria de auto-incremento.
Pasos tomados:
Cree un CharField ScheduledMeasurement.temp_location_unique_id y un TempLocation modelo, y las migraciones para crearlos. TempLocation tiene la estructura que quiero que tenga Location.
Cree una migración de datos que establezca todos los temp_location_unique_id''s usando la clave externa, y que copie todos los datos de Location a TempLocation
Eliminar la clave externa y la tabla de ubicación con una migración
Vuelva a crear el modelo de ubicación de la forma que quiero, vuelva a crear la clave externa con null = True. Renombrado ''unique_id'' a ''location_code'' ...
Cree una migración de datos que complete los datos en Ubicación usando TempLocation, y complete las claves externas en ScheduledMeasurement usando temp_location
Eliminar temp_location, TempLocation y null = True en la clave externa
Y edite todo el código que asumió que unique_id era único (todos los objetos objects.get (unique_id = ...)), y que usaba unique_id de lo contrario ...
Yo mismo he encontrado este problema y terminé escribiendo una migración reutilizable (específica de MySQL) que también tiene en cuenta una relación de muchos a muchos. Como resumen, los pasos que tomé fueron:
Modifique la clase modelo de esta manera:
class Something(models.Model): name = models.CharField(max_length=64, unique=True)
Agrega una nueva migración a lo largo de estas líneas:
app_name = ''app'' model_name = ''something'' related_model_name = ''something_else'' model_table = ''%s_%s'' % (app_name, model_name) pivot_table = ''%s_%s_%ss'' % (app_name, related_model_name, model_name) class Migration(migrations.Migration): operations = [ migrations.AddField( model_name=model_name, name=''id'', field=models.IntegerField(null=True), preserve_default=True, ), migrations.RunPython(do_most_of_the_surgery), migrations.AlterField( model_name=model_name, name=''id'', field=models.AutoField( verbose_name=''ID'', serialize=False, auto_created=True, primary_key=True), preserve_default=True, ), migrations.AlterField( model_name=model_name, name=''name'', field=models.CharField(max_length=64, unique=True), preserve_default=True, ), migrations.RunPython(do_the_final_lifting), ]
dónde
def do_most_of_the_surgery(apps, schema_editor): models = {} Model = apps.get_model(app_name, model_name) # Generate values for the new id column for i, o in enumerate(Model.objects.all()): o.id = i + 1 o.save() models[o.name] = o.id # Work on the pivot table before going on drop_constraints_and_indices_in_pivot_table() # Drop current pk index and create the new one cursor.execute( "ALTER TABLE %s DROP PRIMARY KEY" % model_table ) cursor.execute( "ALTER TABLE %s ADD PRIMARY KEY (id)" % model_table ) # Rename the fk column in the pivot table cursor.execute( "ALTER TABLE %s " "CHANGE %s_id %s_id_old %s NOT NULL" % (pivot_table, model_name, model_name, ''VARCHAR(30)'')) # ... and create a new one for the new id cursor.execute( "ALTER TABLE %s ADD COLUMN %s_id INT(11)" % (pivot_table, model_name)) # Fill in the new column in the pivot table cursor.execute("SELECT id, %s_id_old FROM %s" % (model_name, pivot_table)) for row in cursor: id, key = row[0], row[1] model_id = models[key] inner_cursor = connection.cursor() inner_cursor.execute( "UPDATE %s SET %s_id=%d WHERE id=%d" % (pivot_table, model_name, model_id, id)) # Drop the old (renamed) column in pivot table, no longer needed cursor.execute( "ALTER TABLE %s DROP COLUMN %s_id_old" % (pivot_table, model_name)) def do_the_final_lifting(apps, schema_editor): # Create a new unique index for the old pk column index_prefix = ''%s_id'' % model_table new_index_prefix = ''%s_name'' % model_table new_index_name = index_name.replace(index_prefix, new_index_prefix) cursor.execute( "ALTER TABLE %s ADD UNIQUE KEY %s (%s)" % (model_table, new_index_name, ''name'')) # Finally, work on the pivot table recreate_constraints_and_indices_in_pivot_table()
- Aplicar la nueva migración.
Puedes encontrar el código completo en este repo . También he escrito sobre eso en mi blog .