使用south重构带继承的Django模型
我在想,能不能用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已经创建):
- 架构迁移,重命名tv.VideoFile和movies.VideoFile中将要移动到新media.VideoFile模型的所有字段,可能改成old_name、old_size等等。
- 架构迁移,让tv.VideoFile和movies.VideoFile继承自media.VideoFile。
- 数据迁移,把old_name复制到name,把old_size复制到size等等。
- 架构迁移,删除old_字段。
在我做这些工作之前,你觉得这样可行吗?有没有更好的方法?
如果你感兴趣,这个项目托管在这里:http://code.google.com/p/medianav/
4 个回答
我尝试按照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中分别创建新的类SimplePost和ExtPost(暂时不要删除其他类)。然后也为这两个类创建模式迁移:
$./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
就这样!希望一切顺利,你已经重构了你的模型。
请查看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
。