我想知道 Django 是否可以进行以下迁移 并且仍然保留数据。

前:

我目前有两个应用程序,一个称为电视,一个称为电影,每个应用程序都有一个 VideoFile 模型(此处简化):

电视/模型.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)

电影/模型.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)

后:

因为这两个 videofile 对象非常相似,所以我想消除重复并在一个名为 media 的单独应用程序中创建一个新模型,其中包含通用 VideoFile 类并使用继承来扩展它:

媒体/模型.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)

电视/模型.py:

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

电影/模型.py:

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

所以我的问题是,如何使用 django-south 完成此任务并仍然维护现有数据?

所有这三个应用程序都已由南部迁移管理,根据南部文档,将架构和数据迁移结合起来是不好的做法,他们建议应该通过几个步骤来完成。

我认为可以使用像这样的单独迁移来完成(假设 media.VideoFile 已经创建)

  1. 架构迁移以重命名 tv.VideoFile 和 movie.VideoFile 中的所有字段,这些字段将移动到新的 media.VideoFile 模型,可能会重命名为 old_name、old_size 等
  2. 架构迁移到 tv.VideoFile 和 movie.VideoFile 以从 media.VideoFile 继承
  3. 数据迁移,将old_name复制到name,old_size复制到size等
  4. 方案迁移以删除old_字段

在我完成所有这些工作之前,您认为这可行吗?有没有更好的办法?

如果您有兴趣,该项目托管在此处: http://code.google.com/p/medianav/

有帮助吗?

解决方案

查看 Paul 下面的回复,了解有关与新版本 Django/South 兼容性的一些说明。


这似乎是一个有趣的问题,而且我正在成为南方的忠实粉丝,所以我决定对此进行一些研究。我根据您上面描述的摘要构建了一个测试项目,并成功使用 South 来执行您所询问的迁移。在我们开始编写代码之前,请注意以下几点:

  • South 文档建议分开进行架构迁移和数据迁移。我在这方面也效仿过。

  • 在后端,Django 通过在继承模型上自动创建 OneToOne 字段来表示继承表

  • 理解了这一点,我们的 South 迁移需要手动正确处理 OneToOne 字段,但是,在对此进行实验时,South(或者可能是 Django 本身)似乎无法在具有相同名称的多个继承表上创建 OneToOne 字段。因此,我将电影/电视应用程序中的每个子表重命名为各自的应用程序(即。电影视频文件/显示视频文件)。

  • 在实际的数据迁移代码中,South 似乎更喜欢先创建 OneToOne 字段,然后向其分配数据。在创建过程中将数据分配给 OneToOne 字段会导致 South 感到窒息。(对于南方的所有凉爽来说,这是一个公平的妥协)。

说了这么多,我尝试保留发出的控制台命令的日志。必要时我会插入评论。最终代码在底部。

命令历史

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

出于篇幅考虑,并且由于模型最终看起来总是相同的,因此我将仅使用“电影”应用程序进行演示。

电影/模型.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')

movie/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')

movie/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和南方的变化,这是一个适用于ubuntu 10.10的版本,django 1.2.3和南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

迁移数据

如何去做?首先编写新的app genpost并进行初始化 与南方的迁移:

$./manage.py schemamigration genpost --initial

(我使用 $ 来表示shell提示符,所以不要输入它。)

接下来在post1 / models.py中创建新类 SimplePost ExtPost 和post2 / models.py分别(不要删除其余的类)。 然后为这两个创建schemamigrations:

$./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 schemamigration post1 --auto
$./manage.py schemamigration post2 --auto
$./manage.py migrate

接下来,您可以从post1 / models.py和post2 / models.py中删除现在冗余的部分,然后创建schemamigrations以将表更新为新状态:

<*>

那应该是它!希望这一切都有效,你已经重构了你的模型。

抽象模型

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

可能通用关系也对您有用。

我做了类似的迁移,我选择分多步完成。除了创建多个迁移之外,我还创建了向后迁移,以便在出现问题时提供回退。然后,我抓取了一些测试数据并向前和向后迁移,直到我确定它向前迁移时它正确地出来。最后,我迁移了生产站点。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top