¿Cuál es el mejor enfoque para cambiar las claves primarias en una aplicación Django existente?

Tengo una aplicación que está en modo BETA. El modelo de esta aplicación tiene algunas clases con una primary_key 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

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. Una importante lección aprendida es la siguiente: 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.

  1. Descargue sus datos en archivos JSON. Utilice las propias herramientas internas de django-admin.py 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. Los archivos separados hacen esto un poco más fácil de hacer.

  2. Elimine 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, también podría ser más sencillo, eliminar y volver a insertar estas filas.

  3. Crea el nuevo esquema. Esto solo creará las tablas que están cambiando.

  4. 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.

  1. Renombra todas tus tablas antiguas.

  2. Crea el nuevo esquema completo.

  3. Escriba SQL para migrar todos los datos del esquema antiguo al esquema nuevo. Esto tendrá que reasignar ingeniosamente las teclas a medida que avanza.

  4. Suelte las tablas antiguas renombradas.

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 externa en algún otro modelo para este modelo, cree también un nuevo campo de fk falso en este modelo (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  fill_id 

en esta fecha, llene estos nuevos campos de id 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 externas, elimine también los campos de claves externas anteriores (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.

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 de ddl)

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 el año pasado lo convertí en una clave principal. Por supuesto, no resultaron ser tan únicos como se esperaba en ese momento. También hay un modelo de “Medición progtwigda” que tiene una clave externa a “Ubicación”.

Ahora quiero corregir ese error y dar a Location una clave principal ordinaria de auto-incremento.

Pasos tomados:

  1. 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.

  2. 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

  3. Elimine la clave externa y la tabla de ubicación con una migración

  4. 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’ …

  5. 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

  6. Elimine 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 …

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:

  1. 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.
  2. Migración de datos simple para generar nuevas claves primarias:

     def fill_ids(apps, schema_editor): Model = apps.get_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)] 
  3. Migración de claves externas existentes. Escribí una migración combinada:

     def change_model_fks(apps, schema_editor): Model = apps.get_model('', '') # Our model we want to change primary key for FkModel = apps.get_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='', name='model', field=models.ForeignKey('', 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='', name='model', field=models.IntegerField('', blank=True, null=True) ), ] 
  4. 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 de id

     class Migration(migrations.Migration): dependencies = […] operations = [ # Drop old primary key migrations.AlterField( model_name='', name='', field=models.CharField(max_length=100), ), # Create new primary key migrations.RunSQL( ['ALTER TABLE  CHANGE id id INT (11) NOT NULL PRIMARY KEY AUTO_INCREMENT'], ['ALTER TABLE 
    CHANGE id id INT (11) NULL', 'ALTER TABLE
    DROP PRIMARY KEY'], state_operations=[migrations.AlterField( model_name='', name='id', field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), )] ), # Recreate foreign key constraints migrations.AlterField( model_name='', name='model', field=models.ForeignKey(blank=True, null=True, to='.'), ]

Encontré 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:

  1. Modificar la clase de modelo de esta manera:

     class Something(models.Model): name = models.CharField(max_length=64, unique=True) 
  2. 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() 
    1. Aplicar la nueva migración.

Puedes encontrar el código completo en este repository . También he escrito sobre eso en mi blog .

Tuve que migrar algunas claves en mi aplicación Django 1.11 – las claves antiguas eran deterministas, basadas en un modelo externo. Más tarde, sin embargo, resultó que este modelo externo podría cambiar, por lo que necesitaba mis propios UUID.

Para referencia, estaba cambiando una mesa de botellas de vino específicas para POS, así como una mesa de ventas para esas botellas de vino.

  • Creé un campo extra en todas las tablas relevantes. En el primer paso, necesitaba introducir campos que podrían ser Ninguno, luego generé UUID para todos ellos. A continuación, apliqué un cambio a través de Django donde el nuevo campo UUID se marcó como único. Podría comenzar a migrar todas las vistas, etc. para usar este campo UUID como una búsqueda, por lo que se necesitaría cambiar menos durante la próxima fase, más aterradora de la migración.
  • Actualicé las claves foráneas utilizando una combinación. (en PostgreSQL, no Django)
  • Reemplacé toda mención de las claves antiguas con las nuevas claves y las probé en pruebas unitarias, ya que usan su propia base de datos de pruebas separada. Este paso es opcional para los vaqueros.
  • Al ir a las tablas de PostgreSQL, notará que las restricciones de clave externa tienen nombres de código con números. Necesitas eliminar esas restricciones y hacer otras nuevas:

     alter table pos_winesale drop constraint pos_winesale_pos_item_id_57022832_fk; alter table pos_winesale rename column pos_item_id to old_pos_item_id; alter table pos_winesale rename column placeholder_fk to pos_item_id; alter table pos_winesale add foreign key (pos_item_id) references pos_poswinebottle (id); alter table pos_winesale drop column old_pos_item_id; 
  • Con las nuevas claves externas en su lugar, puede cambiar la clave principal, ya que nada más hace referencia a ella:

     alter table pos_poswinebottle drop constraint pos_poswinebottle_pkey; alter table pos_poswinebottle add primary key (id); alter table pos_poswinebottle drop column older_key; 
  • Falsa la historia de la migración .