2010-01-13 16 views
28

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.¿Cuál es el mejor enfoque para cambiar las claves principales en una aplicación existente de Django?

class Something(models.Model): 
    name = models.CharField(max_length=64, primary_key=True) 

creo que era una mala idea (ver unicode error when saving an object in django admin) y me gustaría ir y tener un ID para cada clase de mi modelo.

class Something(models.Model): 
    name = models.CharField(max_length=64, db_index=True) 

que he hecho los cambios en mi modelo (Cambiar cada primary_key = True por db_index = Verdadero) y quiero migrar la base de datos con south.

Por desgracia, 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 su ayuda

+0

¿Nos puede mostrar su modelo? –

+0

@tomlog: vea http://stackoverflow.com/questions/2011629/unicode-error-when-saving-an-object-in-django-admin Hay un ejemplo. Deseo agregar una identificación como pk – luc

+0

FWIW, no hay nada de malo en tener el nombre de una clave principal, siempre y cuando la base de datos use correctamente los índices. – Tobu

Respuesta

51

De acuerdo, su modelo probablemente sea incorrecto.

La clave primaria formal siempre debe ser una clave sustituta. Nunca nada más. [Palabras fuertes. Soy diseñador de bases de datos desde la década de 1980. Lo importante aprendido es que todo es modificable, incluso cuando los usuarios juran en las tumbas de sus madres que el valor no se puede cambiar es verdaderamente una clave natural que se puede tomar como primaria. No es primario. Solo los sustitutos pueden ser primarios.]

Usted está haciendo una cirugía a corazón abierto. No te metas con la migración de esquema. Usted es reemplazando el esquema.

  1. Descargue sus datos en archivos JSON. Use las herramientas django-admin.py internas de Django para esto. Debe crear un archivo de descarga para cada uno que vaya a cambiar y cada tabla que dependa 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 con respecto al esquema anterior.

    Las tablas que dependen de estas tablas tendrán sus FK modificadas; puede actualizar las filas en su lugar o - podría ser más simple - eliminar y reinsertar estas filas, también.

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

  4. Escribir 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íneas de tupla JSON que se crearon para usted. Luego puede insertarlos en la base de datos.

    Tiene dos casos.

    • Se insertarán las tablas con el cambio de PK modificado y obtendrán nuevas PK. Estos deben ser "conectados en cascada" a otras tablas para asegurar que los FK de la otra mesa también cambien.

    • Las tablas con FK que cambien deberán ubicar la fila en la tabla externa y actualizar su referencia FK.

Alternativa.

  1. Cambie el nombre a todas sus tablas antiguas.

  2. Cree el esquema nuevo completo.

  3. Escribir SQL para migrar todos los datos del esquema anterior al nuevo esquema. Esto tendrá que reasignar inteligentemente las claves a medida que avanza.

  4. Suelta las tablas antiguas renombradas.

 

+0

Cuando dices "clave primaria explícita" ¿Te refieres a una clave primaria formal no definida por Django? Sí y sí, estoy de acuerdo con tus puntos. Me gusta tu primer acercamiento. Cualquier puntero sobre cómo exportar/importar en JSON? – luc

+1

Estoy citando su pregunta @luc; pareces querer decir que intentas crear una clave principal no definida por Django. Esta es una mala idea. –

+0

He editado mi pregunta. Espero que arroje algunas aclaraciones. dumpdata y loaddata parece ser una forma. pero tal vez no sea tan fácil – luc

6

Actualmente están fallando debido a que está agregando una columna pk que rompe los requisitos NOT NULL y único.

Usted debe dividir la migración en several steps, separando las migraciones de esquema y las migraciones de datos:

  • añadir la nueva columna, indexado, pero la clave no primaria, con un valor por defecto (migración DDL)
  • migrar los datos : llenar la nueva columna con el valor correcto (migración de datos)
  • marcan la nueva clave de la columna principal, y quitar la columna de la antigua pk si se ha hecho innecesaria (migración DDL)
+0

Esto no funcionó para mí, debido a problemas con el sur. Sugiero seguir las soluciones de @RemcoGerlich o S.Lott – grokpot

8

Para cambiar la clave principal con el sur se puede utilizar el comando south.db.create_primary_key en datamigration. Para cambiar su pk CharField personalizada con la norma AutoField que debe hacer:

1) crear un nuevo campo en el modelo

class MyModel(Model): 
    id = models.AutoField(null=True) 

1,1) si tiene una clave externa en algún otro modelo de este modelo, cree nuevo campo falso fk en estos modelo demasiado (utilizar IntegerField, que 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 una nueva datamigration

./manage.py datamigration <your_appname> fill_id 

en datamigration tis llenar estos nuevos Identificación y campos fk con los números (solo enumerarlos)

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 su conjunto modelos primary_key = True en su nuevo campo pk

id = models.AutoField(primary_key=True) 

5) eliminar el campo de la clave primaria anterior (si no es necesario) crear auto migra y migrar.

5.1) si tiene las claves externas - Eliminar viejos campos de clave externa también (migrar)

6) último paso - restablecer las relaciones clave fireign. Crear campo FK real otra vez, y eliminar su campo fake_fk, crear la migración automática pero no migran - tiene que modificar la migración ha creado automáticamente (!): En lugar de crear nuevos FK y eliminar fake_fk - cambiar el nombre de 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') 

por lo tanto, 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.

+5

¿Has probado esto realmente? No puede tener un campo de autofield que no sea primario, ni uno que permita null (south no lo permita, y no creo que django lo haga).En consecuencia, no puede hacer búsquedas relacionadas una vez que haya alterado la clave principal. – Marcin

+2

Dicho esto, una versión adaptada de este enfoque me funcionó bien. – Marcin

+4

@marcin, cómo se adaptó el primer paso (para superar la prohibición south/django en un AutoField nulo y no nulo) – hobs

6

Tuve el mismo problema hoy y encontré una solución inspirada en las respuestas anteriores.

Mi modelo tiene una tabla de "Ubicación". Tiene un CharField llamado "unique_id" y tontamente lo convertí en una clave primaria, el año pasado. Por supuesto, no resultaron ser tan únicos como se esperaba en ese momento. También hay un modelo "ScheduledMeasurement" que tiene una clave externa para "Location".

Ahora quiero corregir ese error y darle a la ubicación una clave principal ordinaria de incremento automático.

Las medidas adoptadas:

  1. Crear una ScheduledMeasurement.temp_location_unique_id CharField y un modelo TempLocation, y las migraciones para crearlos. TempLocation tiene la estructura que quiero que tenga la ubicación.

  2. Crear una migración de datos que establece toda la temp_location_unique_id del uso de la clave externa, y que copia todos los datos de un lugar a TempLocation

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

  4. Vuelva a crear el modelo de ubicación de la manera que yo quiero que sea, vuelva a crear la clave externa con nulo = verdadero. A llamarse 'id_exclusivo' a 'location_code' ...

  5. Crear una migración de datos que rellena los datos de ubicación mediante TempLocation, y rellena las claves externas de ScheduledMeasurement usando temp_location

  6. Quitar temp_location, y TempLocation null = True en la clave externa

y editar todo el código que supone id_exclusivo era única (todos los objects.get (id_exclusivo = ...) cosas), y que utilizan id_exclusivo lo contrario ...

1

Me he encontrado con este problema y terminé escribiendo una migración reutilizable (específica de MySQL) que también tiene en cuenta una relación Muchos a Muchos.A modo de resumen, los pasos que tomó fueron:

  1. modificar la clase modelo así:

    class Something(models.Model): 
        name = models.CharField(max_length=64, unique=True) 
    
  2. Agregar 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), 
        ] 
    

    donde

    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

Puede encontrar el código completo en este repo. También he escrito sobre esto en mi blog.

3

Logré hacer esto con django 1.10.4 migrations y mysql 5.5, pero no fue fácil.

Tenía una clave primaria varchar con varias claves externas. Agregué un campo id, datos migrados y claves foráneas. Así es como:

  1. Añadiendo el campo de la clave primaria futura. Agregué un campo id = models.IntegerField(default=0) a mi modelo principal y generé una migración automática.
  2. simple migración de datos para generar nuevas claves principales:

    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)] 
    
  3. Migración de las claves externas existentes. Escribí un combinado de migración:

    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) 
         ), 
        ] 
    
  4. Intercambio de claves primarias y restaurar las claves externas. De nuevo, una migración personalizada. Me auto generada por la base de esta migración, cuando a) Quité primary_key=True de la clave primaria de edad y b) que se retiró id campo

    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>'), 
        ] 
    
Cuestiones relacionadas