Использование 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 это плохая практика для объединения схемы и данных миграция, и они рекомендуют, чтобы это было сделано в несколько шагов.

Я думаю, что это можно было бы сделать с помощью отдельных миграций, таких как эта (предполагая медиа.Видеофайл уже создан)

  1. миграция схемы для переименования всех полей в телевизоре.Видеофайлы и фильмы.Видеофайл, который будет перенесен на новые носители.Модель видеофайла, возможно, к чему-то вроде old_name, old_size и т. д.
  2. миграция схемы на ТВ.Видеофайлы и фильмы.Видеофайл наследуется от медиа.Видеофайл
  3. миграция данных копировать old_name в name, old_size в size и т. д.
  4. схема миграции для удаления полей old_
Прежде чем я пройду через всю эту работу, как вы думаете, это сработает? Есть ли лучший способ?

Если вы заинтересованы, проект размещен здесь: http://code.google.com/p/medianav/

4 32

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:

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)
Очевидно, что существует много совпадений, поэтому я хотел учесть общие черты. в модель general post и только сохранить различия в другой класс моделей.

Новая настройка:

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

Может быть родовое отношение будет полезно и для вас.

Я сделал аналогичную миграцию, и я решил сделать это в несколько шагов. В дополнение к созданию множественных миграций, я также создал обратную миграцию, чтобы обеспечить запасной вариант, если что-то пойдет не так. Затем я собрал некоторые тестовые данные и перенес их вперед и назад, пока не убедился, что они правильно выходят при переносе вперед. Наконец, я перенес место производства.