使用south重构带继承的Django模型

32 投票
4 回答
9197 浏览
提问于 2025-04-15 15:14

我在想,能不能用Django的south来进行下面的迁移,同时保留数据。

之前:

我现在有两个应用,一个叫tv,一个叫movies,每个应用都有一个VideoFile模型(这里简化一下):

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)

之后:

因为这两个videofile对象非常相似,我想去掉重复的部分,创建一个新的模型,放在一个叫media的独立应用里,这个模型包含一个通用的VideoFile类,并通过继承来扩展它:

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进行管理迁移,根据south的文档,把架构迁移和数据迁移放在一起是不好的做法,他们建议应该分几步来完成。

我觉得可以通过以下几个独立的迁移步骤来实现(假设media.VideoFile已经创建):

  1. 架构迁移,重命名tv.VideoFile和movies.VideoFile中将要移动到新media.VideoFile模型的所有字段,可能改成old_name、old_size等等。
  2. 架构迁移,让tv.VideoFile和movies.VideoFile继承自media.VideoFile。
  3. 数据迁移,把old_name复制到name,把old_size复制到size等等。
  4. 架构迁移,删除old_字段。

在我做这些工作之前,你觉得这样可行吗?有没有更好的方法?

如果你感兴趣,这个项目托管在这里:http://code.google.com/p/medianav/

4 个回答

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

也许通用关系对你也会有帮助。

9

我尝试按照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)

如果你想跟着做,首先需要把这些模型导入到south中:

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

迁移数据

该怎么做呢?首先创建新的应用genpost,并用south进行初始迁移:

$./manage.py schemamigration genpost --initial

(我用$来表示命令行提示符,所以不要输入这个。)

接下来在post1/models.py和post2/models.py中分别创建新的类SimplePostExtPost(暂时不要删除其他类)。然后也为这两个类创建模式迁移:

$./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

就这样!希望一切顺利,你已经重构了你的模型。

49

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


这个问题看起来很有趣,我也越来越喜欢South这个工具,所以决定深入研究一下。我根据你描述的内容建立了一个测试项目,并成功使用South进行了你提到的迁移。在我们进入代码之前,这里有几点说明:

  • South的文档建议将架构迁移和数据迁移分开进行。我在这里也遵循了这个建议。

  • 在后台,Django通过在继承模型上自动创建一个一对一字段来表示继承的表。

  • 了解这一点后,我们的South迁移需要手动正确处理这个一对一字段。然而,在实验中发现,South(或者可能是Django本身)无法在多个继承表上创建同名的一对一字段。因此,我将电影/电视应用中的每个子表重命名为与其各自应用相对应的名称(例如:MovieVideoFile/ShowVideoFile)。

  • 在处理实际的数据迁移代码时,似乎South更喜欢先创建一对一字段,然后再给它赋值。在创建时直接给一对一字段赋值会导致South出错。(这对于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

为了节省空间,并且因为模型最终看起来都是一样的,我只会用'movies'应用来演示。

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'

South真棒!

好的,标准声明:你正在处理实时数据。我给你提供了可用的代码,但请使用--db-dry-run来测试你的架构。在尝试任何操作之前,务必备份数据,并且要小心。

兼容性说明

我会保持我原来的信息不变,但South已经将命令manage.py startmigration改成了manage.py schemamigration

撰写回答