Domanda

Mi chiedevo se la seguente migrazione fosse possibile con Django south e conservando ancora i dati.

Prima:

Al momento ho due app, una chiamata tv, una chiamata film, ognuna con un modello VideoFile (semplificato qui):

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)

film / 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)

Dopo

Poiché i due oggetti file video sono così simili, desidero eliminare la duplicazione e creare un nuovo modello in un'app separata chiamata media che contiene una classe VideoFile generica e utilizzare l'ereditarietà per estenderla:

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)

film / models.py:

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

Quindi la mia domanda è: come posso farlo con django-south e conservare ancora i dati esistenti?

Tutte e tre queste app sono già gestite dalle migrazioni del sud e secondo la documentazione del sud è una cattiva pratica combinare uno schema e la migrazione dei dati e raccomandano che dovrebbe essere fatto in pochi passaggi.

Penso che potrebbe essere fatto usando migrazioni separate come questa (supponendo che media.VideoFile sia già stato creato)

  1. Migrazione dello schema per rinominare tutti i campi in tv.VideoFile e film.VideoFile che passerà ai nuovi media.VideoFile Model, forse in qualcosa come old_name, old_size, ecc
  2. Migrazione dello schema su tv.VideoFile e film.VideoFile da ereditare da media.VideoFile
  3. Migrazione dei dati per copiare old_name in name, old_size in size, ecc
  4. Migrazione dello schema per rimuovere i vecchi campi

Prima di passare attraverso tutto quel lavoro, pensi che funzionerà? C'è un modo migliore?

Se sei interessato, il progetto è ospitato qui: http://code.google. com / p / medianav /

È stato utile?

Soluzione

Controlla la risposta di seguito di Paul per alcune note sulla compatibilità con le versioni più recenti di Django / Sud.


Sembrava un problema interessante e sto diventando un grande fan del Sud, quindi ho deciso di approfondire un po '. Ho realizzato un progetto di test sull'abstract di quello che hai descritto sopra e ho usato con successo South per eseguire la migrazione di cui stai chiedendo. Ecco un paio di note prima di arrivare al codice:

  • La documentazione di South suggerisce di separare le migrazioni degli schemi e quelle dei dati. Ho seguito l'esempio in questo.

  • Nel backend, Django rappresenta una tabella ereditata creando automaticamente un campo OneToOne sul modello ereditario

  • Capendo questo, la nostra migrazione del Sud deve gestire correttamente il campo OneToOne manualmente, tuttavia, sperimentando questo sembra che il Sud (o forse lo stesso Django) non possa creare un OneToOne archiviato su più tabelle ereditate con lo stesso nome . Per questo motivo, ho rinominato ogni tabella figlio nell'app film / tv in modo che corrisponda alla propria app (ad es. MovieVideoFile / ShowVideoFile).

  • Giocando con il codice di migrazione dei dati effettivo, sembra che South preferisca creare prima il campo OneToOne e quindi assegnare i dati ad esso. L'assegnazione dei dati al campo OneToOne durante la creazione provoca il soffocamento di South. (Un giusto compromesso per tutto il fresco che è il Sud).

Detto questo, ho cercato di tenere un registro dei comandi della console che venivano emessi. Interrogherò il commento ove necessario. Il codice finale è in fondo.

Cronologia comandi

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

Per motivi di spazio, e dato che i modelli invariabilmente sembrano uguali alla fine, lo dimostrerò solo con l'app "film".

Film / 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')

film / migrazioni / 0002_unified-videofile.py (migrazione schema)

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')

film / migrazione / 0003_videofile-to-movievideofile-data.py (migrazione dei dati)

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'

Il sud è fantastico!

Ok disclaimer standard: hai a che fare con dati in tempo reale. Ti ho dato il codice di lavoro qui, ma per favore usa --db-dry-run per testare il tuo schema. Fai sempre un backup prima di provare qualsiasi cosa, e generalmente fai attenzione.

AVVISO DI COMPATIBILITÀ

Mantiene intatto il mio messaggio originale, ma da allora South ha cambiato il comando manage.py startmigration in manage.py schemamigration .

Altri suggerimenti

Ho provato a esaminare la soluzione delineata da T Stone e mentre penso che sia un superbo antipasto e spieghi come fare le cose, ho riscontrato alcuni problemi.

Penso che per lo più non sia necessario creare più la voce della tabella per la classe genitore, ovvero non è necessario

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

più. Django ora lo farà automaticamente per te (se hai campi non nulli, allora quanto sopra non ha funzionato per me e mi ha dato un errore del database).

Penso che sia probabilmente dovuto ai cambiamenti di Django e del Sud, ecco una versione che ha funzionato per me su Ubuntu 10.10 con Django 1.2.3 e South 0.7.1. I modelli sono leggermente diversi, ma otterrai l'essenza:

Installazione iniziale

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)

Ovviamente ci sono molte sovrapposizioni, quindi volevo considerare i punti in comune in un modello post generale e mantieni solo le differenze nell'altro classi di modelli.

nuova configurazione:

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)

Se vuoi seguire, devi prima portare questi modelli a sud:

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

Migrazione dei dati

Come procedere? Prima scrivi il nuovo genpost dell'app e fai l'iniziale migrazioni con il sud:

$./manage.py schemamigration genpost --initial

(Sto usando $ per rappresentare il prompt delle shell, quindi non digitare quello.)

Quindi creare le nuove classi SimplePost e ExtPost in post1 / models.py e post2 / models.py rispettivamente (non eliminare ancora il resto delle classi). Quindi crea schemi di migrazione anche per questi due:

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

Ora possiamo applicare tutte queste migrazioni:

$./manage.py migrate

Andiamo al nocciolo della questione, migrando i dati da post1 e post2 a genpost:

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

Quindi modifica 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.")

Ora applica queste migrazioni:

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

Quindi puoi eliminare le parti ora ridondanti da post1 / models.py e post2 / models.py e quindi creare schemi per aggiornare le tabelle al nuovo stato:

<*>

E dovrebbe essere quello! Spero che tutto funzioni e hai rifattorizzato i tuoi modelli.

Modello astratto

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

Può essere relazione generica sarà utile anche per te.

Ho fatto una migrazione simile e ho scelto di farlo in più passaggi. Oltre a creare le migrazioni multiple, ho anche creato la migrazione all'indietro per fornire un fallback in caso di problemi. Quindi, ho acquisito alcuni dati di test e li ho migrati avanti e indietro fino a quando non sono stato sicuro che sarebbe uscito correttamente quando ho migrato in avanti. Infine, ho migrato il sito di produzione.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top