Utiliser south pour refactoriser un modèle Django avec héritage
-
05-07-2019 - |
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éé)
- 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.
- Migration du schéma vers tv.VideoFile et movies.VideoFile à hériter de media.VideoFile
- Migration des données pour copier ancien nom en nom, taille ancienne, etc.
- 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 /
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.
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.