Подтвердить что ты не робот

Использование юга для реорганизации модели Django с наследованием

Мне было интересно, возможна ли следующая миграция с Django south и сохранить данные.

До:

В настоящее время у меня есть два приложения, один из которых называется tv, один называется кинофильмами, каждый с моделью VideoFile (упрощенный здесь):

ТВ /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)

фильмы /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, которое содержит общий класс VideoFile и использовать наследование для его расширения:

СМИ /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)

ТВ /models.py:

class VideoFile(media.models.VideoFile):
    show = models.ForeignKey(Show, blank=True, null=True)

фильмы /models.py:

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

Итак, мой вопрос: как я могу выполнить это с помощью django-south и все еще поддерживать существующие данные?

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

Я думаю, что это можно сделать, используя отдельные миграции вроде этого (при условии, что media.VideoFile уже создан)

  • Переход схемы для переименования всех полей в tv.VideoFile и movies.VideoFile, который переместится на новую версию media.VideoFile, возможно, на что-то вроде old_name, old_size и т.д.
  • Переход схемы на tv.VideoFile и movies.VideoFile для наследования из media.VideoFile
  • Перенос данных для копирования old_name для имени, old_size в размер и т.д.
  • Переход схемы для удаления старых полей

Прежде чем я пройду всю эту работу, вы думаете, что это сработает? Есть ли лучший способ?

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

4b9b3361

Ответ 1

Ознакомьтесь с ответом, приведенным ниже Paul для некоторых заметок о совместимости с более новыми версиями Django/South.


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

  • Документация Юга рекомендует выполнять миграцию схем и миграцию данных отдельно. Я следовал этому примеру.

  • На внутреннем сервере Django представляет собой унаследованную таблицу, автоматически создавая поле OneToOne в наследующей модели

  • Понимая это, наша миграция на Юг должна правильно обрабатывать поле OneToOne вручную, однако, экспериментируя с этим, кажется, что Юг (или, возможно, сам Django) не может создать OneToOne, зарегистрированный в нескольких унаследованных таблицах с тем же именем, Из-за этого я переименовал каждый дочерний стол в приложении movie/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')

фильмы/миграции/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')

фильмы/миграция/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'

Юг потрясающий!

Стандартная оговорка Ok: вы имеете дело с данными в реальном времени. Я дал вам рабочий код здесь, но, пожалуйста, используйте --db-dry-run для проверки вашей схемы. Всегда делайте резервную копию, прежде чем пытаться что-либо, и обычно будьте осторожны.

ПОДТВЕРЖДЕНИЕ СОВМЕСТИМОСТИ

Я собираюсь сохранить исходное сообщение неповрежденным, но с тех пор Юг сменил команду manage.py startmigration на manage.py schemamigration.

Ответ 2

Я попытался пройти через решение, описанное T Stone, и, хотя я считаю его превосходным стартером и объясняет, как все должно быть сделано, я столкнулся с несколькими проблемами.

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

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

больше. Django теперь сделает это автоматически для вас (если у вас есть ненулевые поля, то выше не работает для меня и дал мне ошибку базы данных).

Я думаю, что это, вероятно, связано с изменениями в джанго и на юге, вот версия, которая работала для меня на 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)

Очевидно, что существует много совпадений, поэтому я хотел учитывать общие черты в общую модель публикации и только сохранить различия в другой классы моделей.

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

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, а затем создать схемы для обновления таблиц до нового состояния:

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

И это должно быть! Надеюсь, все это сработает, и вы обновили свои модели.

Ответ 3

Абстрактная модель

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

Может быть общее отношение будет полезно и вам.

Ответ 4

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