Использование south для рефакторинга модели Django с наследованием
Мне было интересно, возможна ли следующая миграция с Djangosouth и все еще сохраняют данные.
До:
В настоящее время у меня есть два приложения, одно называется tv, другое-movies, каждое с моделью видеофайла (упрощено здесь):
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)
После:
Поскольку два объекта видеофайла настолько похожи, я хочу избавиться от дублирования и создать новую модель в отдельном приложении вызывается носитель, содержащий универсальный класс видеофайлов и использующий наследование для его расширения:
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)
Таким образом, мой вопрос заключается в том, как я могу сделать это с django-south и все еще поддерживать существующие данные?
Все три этих приложения уже управляются south migrations, и в соответствии с документацией south это плохая практика для объединения схемы и данных миграция, и они рекомендуют, чтобы это было сделано в несколько шагов.
Я думаю, что это можно было бы сделать с помощью отдельных миграций, таких как эта (предполагая медиа.Видеофайл уже создан)
- миграция схемы для переименования всех полей в телевизоре.Видеофайлы и фильмы.Видеофайл, который будет перенесен на новые носители.Модель видеофайла, возможно, к чему-то вроде old_name, old_size и т. д.
- миграция схемы на ТВ.Видеофайлы и фильмы.Видеофайл наследуется от медиа.Видеофайл
- миграция данных копировать old_name в name, old_size в size и т. д.
- схема миграции для удаления полей old_
Если вы заинтересованы, проект размещен здесь: http://code.google.com/p/medianav/
4 ответа:
Проверьте ответ ниже пола для некоторых заметок о совместимости с новыми версиями Django / South.
Это казалось интересной проблемой, и я становлюсь большим поклонником юга, поэтому я решил немного разобраться в этом. Я построил тестовый проект на основе того, что вы описали выше, и успешно использовал Юг для выполнения миграции, о которой вы спрашиваете. Вот пара заметок, прежде чем мы перейдем к коду:
Юг документация рекомендует выполнять миграцию схем и миграцию данных отдельно. Я последовал этому примеру.
На бэкэнде Django представляет унаследованную таблицу, автоматически создавая поле OneToOne в наследующей модели
Понимая это, наша южная миграция должна правильно обрабатывать поле OneToOne вручную, однако, экспериментируя с этим, кажется, что юг (или, возможно, сам Джанго) не может создать поле OneToOne, поданное на нескольких унаследованные таблицы с тем же именем. Из-за этого я переименовал каждую дочернюю таблицу в приложении movies/tv, чтобы она соответствовала его собственному приложению (т. е. MovieVideoFile / ShowVideoFile).
Играя с фактическим кодом миграции данных, кажется, что Юг предпочитает сначала создать поле OneToOne, а затем назначить ему данные. Присвоение данных полю OneToOne во время создания приводит к тому, что Юг задыхается. (Справедливый компромисс для всей прохлады, которая есть на юге).
Итак, имея сказав Все это, я попытался вести журнал выдаваемых консольных команд. Я буду вставлять комментарии, когда это необходимо. Окончательный код находится внизу.
История Команд
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
Ради пространства, и поскольку модели неизменно выглядят одинаково в конце концов, я собираюсь продемонстрировать только с приложением "фильмы".
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 (схема миграция)
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 (миграция данных)
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'
Юг-это потрясающе!
ОК стандартный отказ от ответственности: Вы имеете дело с живыми данными. Я дал вам рабочий код здесь, но, пожалуйста, используйте
--db-dry-run
, чтобы проверить вашу схему. Всегда делайте резервную копию, прежде чем что-либо предпринимать, и вообще будьте осторожны.УВЕДОМЛЕНИЕ О СОВМЕСТИМОСТИ
Я собираюсь сохранить мое первоначальное сообщение нетронутым, но Юг с тех пор изменил команда
manage.py startmigration
вmanage.py schemamigration
.
Я попытался пройти через решение, описанное T Stone, и хотя я думаю, что это превосходное начало и объясняет, как все должно быть сделано, я столкнулся с несколькими проблемами.
Я думаю, что в основном вам больше не нужно создавать запись таблицы для родительского класса, т. е. вам не нужно
new_movie.videofile_ptr = orm['media.VideoFile'].objects.create()
Больше нет. Django теперь будет делать это автоматически для вас (если у вас есть ненулевые поля, то выше не работал для меня и дал мне ошибку базы данных).
Я думаю, что это вероятно, это связано с изменениями в django и south, вот версия, которая работала для меня на ubuntu 10.10 с django 1.2.3 и south 0.7.1. Модели немного отличаются, но вы получите суть:
Начальная настройка
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:
Очевидно, что существует много совпадений, поэтому я хотел учесть общие черты. в модель general post и только сохранить различия в другой класс моделей.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)
Новая настройка:
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)
Если вы хотите следовать дальше, вам сначала нужно будет получить эти модели на юг:
$./manage.py schemamigration post1 --initial $./manage.py schemamigration post2 --initial $./manage.py migrate
Миграция данных
Как это сделать? Сначала напишите новое приложение genpost и выполните начальное миграции с юга:
$./manage.py schemamigration genpost --initial
(я использую
$
для представления оболочек подскажите, так что не печатайте это.)Далее создайте новые классы SimplePost и ExtPost в post1/models.py и еще post2/models.py соответственно (остальные классы пока не удаляйте). Затем создайте схемамиграции и для этих двух:
Теперь мы можем применить все эти миграции:$./manage.py schemamigration post1 --auto $./manage.py schemamigration post2 --auto
$./manage.py migrate
Давайте перейдем к сути вопроса, перенеся данные из post1 и post2 в genpost:
$./manage.py datamigration genpost post1_and_post2_to_genpost --freeze post1 --freeze post2
Затем редактировать 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.")
Теперь примените эти миграции:
$./manage.py migrate
Далее вы можете удалить теперь избыточные части из post1/models.py и еще post2/models.py а затем создайте schemamigrations для обновления таблиц до нового состояния:
$./manage.py schemamigration post1 --auto $./manage.py schemamigration post2 --auto $./manage.py migrate
И это должно быть так! Надеюсь, все это работает,и вы изменили свои модели.
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
Может быть родовое отношение будет полезно и для вас.
Я сделал аналогичную миграцию, и я решил сделать это в несколько шагов. В дополнение к созданию множественных миграций, я также создал обратную миграцию, чтобы обеспечить запасной вариант, если что-то пойдет не так. Затем я собрал некоторые тестовые данные и перенес их вперед и назад, пока не убедился, что они правильно выходят при переносе вперед. Наконец, я перенес место производства.