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)
- 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.
- Esquema de migración a tv.VideoFile y películas.VideoFile para heredar de media.VideoFile
- Migración de datos para copiar old_name a name, old_size a size, etc.
- 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.
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
.