python django migration django-south

python - Usando el sur para refactorizar un modelo de Django con herencia



migration django-south (4)

Hice una migración similar y elegí hacerlo en varios pasos. Además de crear las migraciones múltiples, también creé la migración hacia atrás para proporcionar una alternativa si las cosas iban mal. Luego, tomé algunos datos de prueba y los migré hacia delante y hacia atrás hasta que estuve seguro de que salían correctamente cuando emigré hacia adelante. Finalmente, migré el sitio de producción.

Me preguntaba si la siguiente migración es posible con Django south y aún retener datos.

Antes de:

Actualmente tengo dos aplicaciones, una llamada televisión, una llamada películas, cada una con un modelo de VideoFile (simplificado aquí):

tv / models.py:

class VideoFile(models.Model): show = models.ForeignKey(Show, blank=True, null=True) name = models.CharField(max_length=1024, blank=True) size = models.IntegerField(blank=True, null=True) ctime = models.DateTimeField(blank=True, null=True)

películas / models.py:

class VideoFile(models.Model): movie = models.ForeignKey(Movie, blank=True, null=True) name = models.CharField(max_length=1024, blank=True) size = models.IntegerField(blank=True, null=True) ctime = models.DateTimeField(blank=True, null=True)

Después:

Debido a que los dos objetos de videofile son tan similares, quiero deshacerme de la duplicación y crear un nuevo modelo en una aplicación separada llamada media que contenga una clase genérica de VideoFile y use la herencia para extenderla:

media / models.py:

class VideoFile(models.Model): name = models.CharField(max_length=1024, blank=True) size = models.IntegerField(blank=True, null=True) ctime = models.DateTimeField(blank=True, null=True)

tv / models.py:

class VideoFile(media.models.VideoFile): show = models.ForeignKey(Show, blank=True, null=True)

películas / models.py:

class VideoFile(media.models.VideoFile): movie = models.ForeignKey(Movie, blank=True, null=True)

Entonces mi pregunta es, ¿cómo puedo lograr esto con django-sur y aún mantener los datos existentes?

Estas tres aplicaciones ya están administradas por migraciones del sur y, de acuerdo con la documentación del sur, es una mala práctica combinar un esquema y migración de datos, y recomiendan que se realice en unos pocos pasos.

Creo que se podría hacer usando migraciones separadas como esta (asumiendo que media.VideoFile ya está creado)

  1. Esquema de migración para cambiar el nombre de todos los campos en tv.VideoFile y movies.VideoFile que se moverá al nuevo modelo de media.VideoFile, tal vez a algo como old_name, old_size, etc.
  2. Esquema de migración a tv.VideoFile y películas.VideoFile para heredar de media.VideoFile
  3. Migración de datos para copiar old_name a name, old_size a size, etc.
  4. Esquema de migración para eliminar campos antiguos

Antes de pasar por todo ese trabajo, ¿crees que funcionará? ¿Hay una mejor manera?

Si está interesado, el proyecto está alojado aquí: http://code.google.com/p/medianav/


Intenté analizar la solución planteada por T Stone y, aunque creo que es un excelente comienzo y explica cómo se deben hacer las cosas, tuve algunos problemas.

Creo que, en general, ya no es necesario crear la entrada de tabla para la clase principal, es decir, no es necesario

new_movie.videofile_ptr = orm[''media.VideoFile''].objects.create()

nunca más. Django lo hará automáticamente por usted (si tiene campos no nulos, lo anterior no funcionó y me dio un error en la base de datos).

Creo que probablemente se deba a cambios en django y sur, aquí hay una versión que funcionó para mí en ubuntu 10.10 con django 1.2.3 y sur 0.7.1. Los modelos son un poco diferentes, pero obtendrás la esencia:

Configuración inicial

post1 / models.py:

class Author(models.Model): first = models.CharField(max_length=30) last = models.CharField(max_length=30) class Tag(models.Model): name = models.CharField(max_length=30, primary_key=True) class Post(models.Model): created_on = models.DateTimeField() author = models.ForeignKey(Author) tags = models.ManyToManyField(Tag) title = models.CharField(max_length=128, blank=True) content = models.TextField(blank=True)

post2 / models.py:

class Author(models.Model): first = models.CharField(max_length=30) middle = models.CharField(max_length=30) last = models.CharField(max_length=30) class Tag(models.Model): name = models.CharField(max_length=30) class Category(models.Model): name = models.CharField(max_length=30) class Post(models.Model): created_on = models.DateTimeField() author = models.ForeignKey(Author) tags = models.ManyToManyField(Tag) title = models.CharField(max_length=128, blank=True) content = models.TextField(blank=True) extra_content = models.TextField(blank=True) category = models.ForeignKey(Category)

Obviamente, hay mucha superposición, por lo que quería factorizar las características comunes en un modelo de publicación general y solo mantener las diferencias en las otras clases de modelos.

nueva configuración:

genpost / models.py:

class Author(models.Model): first = models.CharField(max_length=30) middle = models.CharField(max_length=30, blank=True) last = models.CharField(max_length=30) class Tag(models.Model): name = models.CharField(max_length=30, primary_key=True) class Post(models.Model): created_on = models.DateTimeField() author = models.ForeignKey(Author) tags = models.ManyToManyField(Tag) title = models.CharField(max_length=128, blank=True) content = models.TextField(blank=True)

post1 / models.py:

import genpost.models as gp class SimplePost(gp.Post): class Meta: proxy = True

post2 / models.py:

import genpost.models as gp class Category(models.Model): name = models.CharField(max_length=30) class ExtPost(gp.Post): extra_content = models.TextField(blank=True) category = models.ForeignKey(Category)

Si desea seguir, primero deberá obtener estos modelos en el sur:

$./manage.py schemamigration post1 --initial $./manage.py schemamigration post2 --initial $./manage.py migrate

Migrando los datos

¿Cómo hacerlo? Primero escribe la nueva aplicación genpost y realiza las migraciones iniciales con south:

$./manage.py schemamigration genpost --initial

(Estoy usando $ para representar el mensaje de shells, así que no escriba eso).

A continuación, cree las nuevas clases SimplePost y ExtPost en post1 / models.py y post2 / models.py respectivamente (no elimine el resto de las clases todavía). A continuación, cree schemamigrations para estos dos también:

$./manage.py schemamigration post1 --auto $./manage.py schemamigration post2 --auto

Ahora podemos aplicar todas estas migraciones:

$./manage.py migrate

Vamos al meollo de la cuestión, migrando los datos de post1 y post2 a genpost:

$./manage.py datamigration genpost post1_and_post2_to_genpost --freeze post1 --freeze post2

Luego edita genpost / migrations / 0002_post1_and_post2_to_genpost.py:

class Migration(DataMigration): def forwards(self, orm): # # Migrate common data into the new genpost models # for auth1 in orm[''post1.author''].objects.all(): new_auth = orm.Author() new_auth.first = auth1.first new_auth.last = auth1.last new_auth.save() for auth2 in orm[''post2.author''].objects.all(): new_auth = orm.Author() new_auth.first = auth2.first new_auth.middle = auth2.middle new_auth.last = auth2.last new_auth.save() for tag in orm[''post1.tag''].objects.all(): new_tag = orm.Tag() new_tag.name = tag.name new_tag.save() for tag in orm[''post2.tag''].objects.all(): new_tag = orm.Tag() new_tag.name = tag.name new_tag.save() for post1 in orm[''post1.post''].objects.all(): new_genpost = orm.Post() # Content new_genpost.created_on = post1.created_on new_genpost.title = post1.title new_genpost.content = post1.content # Foreign keys new_genpost.author = orm[''genpost.author''].objects.filter(/ first=post1.author.first,last=post1.author.last)[0] new_genpost.save() # Needed for M2M updates for tag in post1.tags.all(): new_genpost.tags.add(/ orm[''genpost.tag''].objects.get(name=tag.name)) new_genpost.save() post1.delete() for post2 in orm[''post2.post''].objects.all(): new_extpost = p2.ExtPost() new_extpost.created_on = post2.created_on new_extpost.title = post2.title new_extpost.content = post2.content # Foreign keys new_extpost.author_id = orm[''genpost.author''].objects.filter(/ first=post2.author.first,/ middle=post2.author.middle,/ last=post2.author.last)[0].id new_extpost.extra_content = post2.extra_content new_extpost.category_id = post2.category_id # M2M fields new_extpost.save() for tag in post2.tags.all(): new_extpost.tags.add(tag.name) # name is primary key new_extpost.save() post2.delete() # Get rid of author and tags in post1 and post2 orm[''post1.author''].objects.all().delete() orm[''post1.tag''].objects.all().delete() orm[''post2.author''].objects.all().delete() orm[''post2.tag''].objects.all().delete() def backwards(self, orm): raise RuntimeError("No backwards.")

Ahora aplica estas migraciones:

$./manage.py migrate

A continuación, puede eliminar las partes ahora redundantes de post1 / models.py y post2 / models.py y luego crear schemamigrations para actualizar las tablas al nuevo estado:

$./manage.py schemamigration post1 --auto $./manage.py schemamigration post2 --auto $./manage.py migrate

¡Y así debería ser! Afortunadamente, todo funciona y usted ha refabricado sus modelos.


Modelo abstracto

class VideoFile(models.Model): name = models.CharField(max_length=1024, blank=True) size = models.IntegerField(blank=True, null=True) ctime = models.DateTimeField(blank=True, null=True) class Meta: abstract = True

Puede ser la relación genérica también será útil para usted.


Consulte la respuesta a continuación de Paul para obtener algunas notas sobre la compatibilidad con las versiones más recientes de Django / South.

Esto me pareció un problema interesante, y me estoy convirtiendo en un gran admirador de South, así que decidí analizar esto un poco. Creé un proyecto de prueba sobre el resumen de lo que describiste arriba, y utilicé South con éxito para realizar la migración sobre la que preguntas. Aquí hay un par de notas antes de llegar al código:

  • La documentación de South recomienda realizar migraciones de esquema y migraciones de datos por separado. He seguido el ejemplo en esto.

  • En el backend, Django representa una tabla heredada al crear automáticamente un campo OneToOne en el modelo heredado

  • Entendiendo esto, nuestra migración Sur necesita manejar adecuadamente el campo OneToOne manualmente, sin embargo, al experimentar con esto parece que South (o tal vez Django mismo) no puede crear un OneToOne archivado en múltiples tablas heredadas con el mismo nombre. Debido a esto, cambié el nombre de cada una de las tablas secundarias en la aplicación de películas / televisión para que sean respectivas a su propia aplicación (es decir, MovieVideoFile / ShowVideoFile).

  • Al jugar con el código de migración de datos real, parece que South prefiere crear primero el campo OneToOne y luego asignarle datos. Al asignar datos al campo OneToOne durante la creación, South se ahogue. (Un compromiso justo para toda la frescura que es Sur).

Habiendo dicho todo eso, traté de mantener un registro de los comandos de la consola que se emiten. Intercalaré comentarios cuando sea necesario. El código final está en la parte inferior.

Historial de Comando

django-admin.py startproject southtest manage.py startapp movies manage.py startapp tv manage.py syncdb manage.py startmigration movies --initial manage.py startmigration tv --initial manage.py migrate manage.py shell # added some fake data... manage.py startapp media manage.py startmigration media --initial manage.py migrate # edited code, wrote new models, but left old ones intact manage.py startmigration movies unified-videofile --auto # create a new (blank) migration to hand-write data migration manage.py startmigration movies videofile-to-movievideofile-data manage.py migrate # edited code, wrote new models, but left old ones intact manage.py startmigration tv unified-videofile --auto # create a new (blank) migration to hand-write data migration manage.py startmigration tv videofile-to-movievideofile-data manage.py migrate # removed old VideoFile model from apps manage.py startmigration movies removed-videofile --auto manage.py startmigration tv removed-videofile --auto manage.py migrate

Por el bien del espacio, y dado que los modelos invariablemente se ven iguales al final, solo voy a demostrar con la aplicación ''películas''.

películas / models.py

from django.db import models from media.models import VideoFile as BaseVideoFile # This model remains until the last migration, which deletes # it from the schema. Note the name conflict with media.models class VideoFile(models.Model): movie = models.ForeignKey(Movie, blank=True, null=True) name = models.CharField(max_length=1024, blank=True) size = models.IntegerField(blank=True, null=True) ctime = models.DateTimeField(blank=True, null=True) class MovieVideoFile(BaseVideoFile): movie = models.ForeignKey(Movie, blank=True, null=True, related_name=''shows'')

movies / migrations / 0002_unified-videofile.py (migración de esquema)

from south.db import db from django.db import models from movies.models import * class Migration: def forwards(self, orm): # Adding model ''MovieVideoFile'' db.create_table(''movies_movievideofile'', ( (''videofile_ptr'', orm[''movies.movievideofile:videofile_ptr'']), (''movie'', orm[''movies.movievideofile:movie'']), )) db.send_create_signal(''movies'', [''MovieVideoFile'']) def backwards(self, orm): # Deleting model ''MovieVideoFile'' db.delete_table(''movies_movievideofile'')

movies / migration / 0003_videofile-to-movievideofile-data.py (migración de datos)

from south.db import db from django.db import models from movies.models import * class Migration: def forwards(self, orm): for movie in orm[''movies.videofile''].objects.all(): new_movie = orm.MovieVideoFile.objects.create(movie = movie.movie,) new_movie.videofile_ptr = orm[''media.VideoFile''].objects.create() # videofile_ptr must be created first before values can be assigned new_movie.videofile_ptr.name = movie.name new_movie.videofile_ptr.size = movie.size new_movie.videofile_ptr.ctime = movie.ctime new_movie.videofile_ptr.save() def backwards(self, orm): print ''No Backwards''

¡South es asombroso!

Ok descargo de responsabilidad estándar: se trata de datos en vivo. Te di el código de trabajo aquí, pero usa el --db-dry-run para probar tu esquema. Siempre haga una copia de seguridad antes de intentar cualquier cosa, y generalmente tenga cuidado.

AVISO DE COMPATIBILIDAD

Voy a mantener mi mensaje original intacto, pero South ha cambiado el comando manage.py startmigration en manage.py schemamigration .