32 votes

Utiliser south pour refactoriser un modèle Django avec héritage

Je me demandais si la suite de la migration est possible avec Django sud et toujours conserver les données.

Avant:

J'ai actuellement deux applications, l'une appelée la télévision, l'une appelée films, chacun avec un VideoFile modèle (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)

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

Après:

Parce que les deux videofile objets sont tellement semblables que je veux me débarrasser de la duplication et de créer un nouveau modèle dans une application à part, appelé les médias qui contient un générique VideoFile classe et utiliser l'héritage pour é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)

movies/models.py:

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

Donc ma question est, comment puis-je réaliser cela avec django-sud et toujours conserver les données existantes?

Tous les trois de ces applications sont déjà gérés par le sud migrations et selon le sud de la documentation c'est une mauvaise pratique de combiner un schéma et la migration de données et ils recommandent que cela devrait être fait en quelques étapes.

Je pense qu'il pourrait être fait à l'aide des migrations distinctes comme cela (en supposant que les médias.VideoFile est déjà créé)

  1. Schéma de migration pour renommer tous les champs de la télévision.VideoFile et des films.VideoFile qui vont se déplacer vers les nouveaux médias.VideoFile modèle, peut-être à quelque chose comme old_name, old_size, etc
  2. Schéma de la migration vers la télévision.VideoFile et des films.VideoFile d'hériter de médias.VideoFile
  3. La migration des données à copier old_name de nom, old_size à la taille, etc
  4. Schéma de la migration pour supprimer old_ champs

Avant de m'en aller à travers tout ce travail, tu crois que ça va fonctionner? Est-il un meilleur moyen?

Si vous êtes intéressés, le projet est hébergé ici: http://code.google.com/p/medianav/

49voto

T. Stone Points 10782

Découvrez la réponse ci-dessous par Paul pour quelques notes sur la compatibilité avec les nouvelles versions de Django/Sud.


Cela semblait être un problème intéressant, et je suis devenu un grand fan de Sud, j'ai donc décidé de chercher un peu. J'ai construit un projet de test sur le résumé de ce que vous avez décrit ci-dessus, et ont utilisé avec succès le Sud pour effectuer la migration vous parlez. Voici un couple de notes avant d'entrer le code:

  • Le Sud de la documentation recommande de faire le schéma des migrations et les migrations de données séparée. J'ai emboîté le pas dans cette.

  • Sur le backend, Django représente hérité d'un tableau en créant automatiquement un OneToOne champ sur l'héritage du modèle

  • De ce constat, notre région du Sud de la migration doit gérer correctement la OneToOne champ manuellement, cependant, à expérimenter avec cela, il semble que le Sud (ou peut-être de Django lui-même) ne peut pas créer un OneToOne déposée sur plusieurs hérité de tables avec le même nom. De ce fait, j'ai renommé chaque enfant-table dans les films/tv de l'application respectifs à sa propre application (ie. MovieVideoFile/ShowVideoFile).

  • En jouant avec les données réelles de la migration de code, il semble que le Sud préfère créer le OneToOne premier champ, puis affecter les données. L'affectation des données de la OneToOne terrain lors de la création de la cause du Sud à s'étouffer. (Un compromis équitable pour toute la fraîcheur qui se trouve au Sud).

Donc, après avoir dit tout cela, j'ai essayé de tenir un journal de la console de commandes. Je vais intervenir commentaire si nécessaire. Le code final est en bas.

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

Pour l'espace de saké, et depuis les modèles est toujours le même aspect dans la fin, je ne vais démontrer avec des "films" app.

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

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 (la migration des 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'

Sud est génial!

Ok standard avertissement: Vous êtes en relation avec des données en direct. Je vous ai donné un code de travail ici, mais s'il vous plaît utiliser l' --db-dry-run pour tester votre schéma. Toujours faire une sauvegarde avant de tenter quoi que ce soit, et plus généralement d'être prudent.

AVIS SUR LA COMPATIBILITÉ

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

9voto

Paul Points 3633

Je l'ai fait essayer à marcher à travers la solution décrite par T la Pierre et le bien que je pense que c'est une superbe starter et explique la façon de faire les choses que j'ai rencontré quelques problèmes.

Je pense surtout que vous n'avez pas besoin de créer de l'entrée de la table de la classe parent plus, c'est à dire que vous n'avez pas besoin

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

plus. Django va maintenant faire cela automatiquement pour vous (si vous avez les champs non null alors le ci-dessus ne fonctionne pas pour moi et m'a donné une erreur de base de données).

Je pense que c'est probablement en raison de changements dans django et du sud, voici une version qui a fonctionné pour moi sur ubuntu 10.10 avec django, 1.2.3 et sud 0.7.1. Les modèles sont un peu différentes, mais vous obtenez 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 ya évidemment beaucoup de chevauchement, donc je voulais facteur de points communs dans un poste de général du modèle et de ne garder que les différences dans l'autre modèle de classes.

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 devrez d'abord obtenir ces modèles dans le sud:

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

La migration des données

Comment aller à ce sujet? La première écriture de la nouvelle application genpost et faire les premiers les migrations avec les pays du sud:

$./manage.py schemamigration genpost --initial

(Je suis aide d' $ pour représenter les coquilles invite, donc ne tapez pas que.)

Créez ensuite le nouvelles classes SimplePost et ExtPost dans post1/models.py et post2/models.py respectivement (ne pas supprimer le reste de la classe encore). Puis créer schemamigrations pour ces deux ainsi:

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

Maintenant, nous pouvons appliquer l'ensemble de ces migrations:

$./manage.py migrate

Entrons dans le vif du sujet, la migration des données à partir de post1 et post2 à genpost:

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

Puis modifier 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.")

Maintenant appliquer ces migrations:

$./manage.py migrate

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

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

Et cela devrait suffire! Espérons que tout cela fonctionne et que vous avez refait votre modèles.

3voto

Oduvan Points 1039

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 relation générique sera utile pour vous aussi.

1voto

scompt.com Points 11304

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.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X