Использование юга для рефакторинга модели Django с наследованием - PullRequest
32 голосов
/ 21 октября 2009

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

До:

В настоящее время у меня есть два приложения, одно из которых называется ТВ, другое - фильмы, каждое с моделью 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 уже создан)

  1. Миграция схемы для переименования всех полей в tv.VideoFile и movies.VideoFile, которые будут перемещены на новое media.VideoFile модель, возможно, на что-то вроде old_name, old_size и т. Д.
  2. Миграция схемы на тв.Видеофайл и фильмы.Видеофайл для наследования от медиа.Видеофайл
  3. Перенос данных для копирования old_name в name, old_size в size и т. Д.
  4. Схема миграции для удаления старых_ полей

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

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

Ответы [ 4 ]

49 голосов
/ 22 октября 2009

Ознакомьтесь с ответом Пола, чтобы узнать о совместимости с более новыми версиями Django / South.


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

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

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

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

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

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

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

9 голосов
/ 26 января 2011

Я пытался пройтись по решению, описанному 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)

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

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

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 голосов
/ 21 октября 2009

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

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

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

1 голос
/ 21 октября 2009

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

...