Question

Je me demandais si la migration suivante était possible avec Django sud tout en conservant les données.

Avant:

J'ai actuellement deux applications, une appelée tv et une autre appelée films, chacune avec un modèle VideoFile (simplifié ici):

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)

films / modèles.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)

Après:

Les deux objets vidéofile étant si similaires, je souhaite supprimer les doublons et créer un nouveau modèle dans une application distincte appelée média contenant une classe VideoFile générique et utiliser l'héritage pour l'étendre:

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)

films / modèles.py:

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

Ma question est donc la suivante: comment puis-je accomplir cela avec django-south tout en conservant les données existantes?

Ces trois applications sont déjà gérées par les migrations vers le sud. Selon la documentation relative au sud, il est déconseillé de combiner un schéma et une migration de données. Ils recommandent donc de le faire en quelques étapes.

Je pense que cela pourrait être fait en utilisant des migrations séparées comme celle-ci (en supposant que media.VideoFile est déjà créé)

  1. Migration de schéma pour renommer tous les champs de tv.VideoFile et movies.VideoFile qui seront transférés vers le nouveau modèle media.VideoFile, par exemple, quelque chose comme ancien_nom, ancien_size, etc.
  2. Migration du schéma vers tv.VideoFile et movies.VideoFile à hériter de media.VideoFile
  3. Migration des données pour copier ancien nom en nom, taille ancienne, etc.
  4. Migration de schéma pour supprimer les anciens champs

Avant de passer à travers tout ce travail, pensez-vous que cela fonctionnera? Y a-t-il un meilleur moyen?

Si cela vous intéresse, le projet est hébergé ici: http://code.google. com / p / medianav /

Était-ce utile?

La solution

Consultez la réponse ci-dessous de Paul pour quelques notes sur la compatibilité avec les nouvelles versions de Django / South.

Cela semblait être un problème intéressant, et je deviens un grand fan de South, alors j’ai décidé d’examiner la question un peu. J'ai construit un projet test sur le résumé de ce que vous avez décrit ci-dessus et j'ai utilisé avec succès South pour effectuer la migration dont vous parlez. Voici quelques notes avant d’arriver au code:

  • La documentation South recommande de séparer les migrations de schéma et les migrations de données. J'ai suivi le mouvement.

  • Sur le backend, Django représente une table héritée en créant automatiquement un champ OneToOne sur le modèle hérité

  • Comprenant cela, notre migration vers le Sud doit gérer correctement le champ OneToOne manuellement. Cependant, en l'expérimentant, il semble que South (ou peut-être Django lui-même) ne puisse pas créer un fichier OneToOne classé dans plusieurs tables héritées portant le même nom. . À cause de cela, j'ai renommé chaque table enfant de l'application movies / tv afin qu'elle corresponde à sa propre application (c'est-à-dire MovieVideoFile / ShowVideoFile).

  • En jouant avec le code de migration de données actuel, il semble que Sud préfère créer le champ OneToOne en premier, puis lui attribuer des données. L'attribution de données au champ OneToOne lors de la création provoque un étouffement de South. (Un juste compromis pour toute la fraîcheur du sud).

Cela dit, j’ai essayé de conserver un journal des commandes de la console en cours d’exécution. Je vais intervenir commentaire si nécessaire. Le code final est en bas.

Historique des commandes

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

Par souci d’espace, et puisque les modèles se ressemblent invariablement, je ne démontrerai qu’avec l’application 'movies'.

movies / 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 (migration de schéma)

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 (migration de données)

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'

Le sud est génial!

Ok clause de non-responsabilité: vous utilisez des données en direct. Je vous ai donné le code de travail ici, mais veuillez utiliser - db-dry-run pour tester votre schéma. Faites toujours une sauvegarde avant d’essayer quoi que ce soit, et soyez généralement prudent.

AVIS DE COMPATIBILITÉ

Je vais garder mon message d'origine intact, mais Sud a depuis modifié la commande manage.py startmigration en manage.py schemamigration .

Autres conseils

J’ai essayé de passer en revue la solution exposée par T Stone et bien que je pense que c’est une excellente entrée en matière et que j’explique comment faire, j’ai rencontré quelques problèmes.

Je pense que la plupart du temps vous n'avez pas besoin de créer l'entrée de table pour la classe parente, c'est-à-dire que vous n'avez pas besoin

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

plus. Django le fera maintenant automatiquement pour vous (si vous avez des champs non nuls, alors ce qui précède ne fonctionne pas pour moi et me donne une erreur de base de données).

Je pense que cela est probablement dû aux changements intervenus dans django et dans le sud. Voici une version qui a fonctionné pour moi dans Ubuntu 10.10 avec django 1.2.3 et dans le sud de 0.7.1. Les modèles sont un peu différents, mais vous en comprendrez l'essentiel:

Configuration initiale

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)

Il y a évidemment beaucoup de chevauchement, je voulais donc prendre en compte les points communs dans un modèle poste général et ne garder que les différences dans l'autre classes de modèles.

nouvelle configuration:

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 vous souhaitez suivre, vous devez d'abord installer ces modèles au sud:

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

Migration des données

Comment s'y prendre? Commencez par écrire la nouvelle application genpost et faites l'initiale migrations avec sud:

$./manage.py schemamigration genpost --initial

(J'utilise $ pour représenter l'invite des shells, alors ne tapez pas.)

Créez ensuite les nouvelles classes SimplePost et ExtPost dans post1 / models.py. et post2 / models.py respectivement (ne supprimez pas encore le reste des classes). Créez ensuite des migrations de migration pour ces deux personnes également:

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

Nous pouvons maintenant appliquer toutes ces migrations:

$./manage.py migrate

Allons au cœur du problème en migrant les données de post1 et post2 vers genpost:

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

Modifiez ensuite 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.")

Appliquez maintenant ces migrations:

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

Ensuite, vous pouvez supprimer les parties désormais redondantes de post1 / models.py et post2 / models.py, puis créer des schemamigrations pour mettre à jour les tables dans le nouvel état:

<*>

Et ça devrait être ça! J'espère que tout fonctionne et que vous avez restructuré vos modèles.

Modèle abstrait

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

Peut-être une relation générique vous sera utile également.

J'ai effectué une migration similaire et j'ai choisi de le faire en plusieurs étapes. En plus de créer les migrations multiples, j'ai également créé la migration vers l'arrière pour fournir un repli en cas de problème. Ensuite, j'ai récupéré des données de test et les ai migrées en avant et en arrière jusqu'à ce que je sois sûr qu'elles sortaient correctement lorsque j'ai migré en avant. Enfin, j'ai migré le site de production.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top