Me preguntaba si la siguiente migración es posible con Django sur y aún conserva los datos.

Antes:

Actualmente tengo dos aplicaciones, una llamada tv, 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 / modelos.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 archivo de video 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 usar 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 / modelos.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-south y aún así mantener los datos existentes?

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

Creo que podría hacerse utilizando migraciones separadas como esta (suponiendo que los medios. VideoFile ya está creado)

  1. Esquema de migración para cambiar el nombre de todos los campos en tv.VideoFile y películas.Video File que se moverá a los nuevos medios.VideoFile model, quizás a algo como old_name, old size, etc.
  2. Esquema de migración a tv.Video File and movies.Video File para heredar de media.VideoFile
  3. Migración de datos para copiar old_name a name, old_size to size, etc.
  4. Esquema de migración para eliminar old_ fields

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 /

32
Andre Miller 21 oct. 2009 a las 15:05

4 respuestas

La mejor respuesta

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 parecía un problema interesante, y me estoy convirtiendo en un gran admirador de South, así que decidí investigar esto un poco. Construí un proyecto de prueba en el resumen de lo que ha descrito anteriormente, y he utilizado con éxito South para realizar la migración que está solicitando. Aquí hay un par de notas antes de llegar al código:

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

  • En el backend, Django representa una tabla heredada creando 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 Sur (o tal vez Django) no puede crear un OneToOne archivado en múltiples tablas heredadas con el mismo nombre. Debido a esto, cambié el nombre de cada tabla secundaria en la aplicación de películas / televisión para que sea respectiva 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. Asignar datos al campo OneToOne durante la creación hace que South se ahogue. (Un compromiso justo para toda la frescura que es el sur).

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

Historial de comandos

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 siempre se ven iguales al final, solo voy a demostrar con la aplicación 'películas'.

Peliculas / modelos.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')

Películas / migración / 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'

¡El sur es asombroso!

Ok descargo de responsabilidad estándar: se trata de datos en vivo. Te he dado un código de trabajo aquí, pero utiliza --db-dry-run para probar tu esquema. Siempre haga una copia de seguridad antes de intentar cualquier cosa y, en general, 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.

49
Jon Cage 11 jun. 2012 a las 22:11

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 un respaldo si las cosas salían mal. Luego, tomé algunos datos de prueba y los migré hacia adelante y hacia atrás hasta estar seguro de que salían correctamente cuando migré hacia adelante. Finalmente, migré el sitio de producción.

1
Edward Dale 21 oct. 2009 a las 13:25

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 relación genérica también será útil para usted.

3
Oduvan 21 oct. 2009 a las 11:12

Intenté analizar la solución esbozada por T Stone y, aunque creo que es un excelente comienzo y explica cómo deben hacerse las cosas, me encontré con algunos problemas.

Creo que, sobre todo, no necesita crear la entrada de la tabla para la clase principal, es decir, no necesita

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

Nunca más. Django ahora hará esto automáticamente por usted (si tiene campos no nulos, entonces lo anterior no funcionó para mí 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 me funcionó en ubuntu 10.10 con django 1.2.3 y sur 0.7.1. Los modelos son un poco diferentes, pero obtendrá 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, así que quería factorizar los puntos en común. en un modelo de publicación general y solo mantiene las diferencias en el otro clases modelo

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á llevar estos modelos al sur:

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

Migrando los datos

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

$./manage.py schemamigration genpost --initial

(Estoy usando $ para representar el indicador 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). Luego, cree también un esquema de migraciones para estos dos:

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

Ahora podemos aplicar todas estas migraciones:

$./manage.py migrate

Vayamos al meollo del asunto, migrando los datos de post1 y post2 a genpost:

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

Luego edite 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 aplique 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 esquemas de migración para actualizar las tablas al nuevo estado:

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

¡Y eso debería ser! Esperemos que todo funcione y que hayas refactorizado tus modelos.

9
Paul 26 ene. 2011 a las 14:42