【问题标题】:Using south to refactor a Django model with inheritance使用 south 重构具有继承的 Django 模型
【发布时间】:2010-12-08 16:17:46
【问题描述】:

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

之前:

我目前有两个应用程序,一个叫 tv,一个叫电影,每个都有一个 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/

【问题讨论】:

    标签: python django migration django-south


    【解决方案1】:

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


    这似乎是一个有趣的问题,而且我正在成为 South 的忠实粉丝,所以我决定稍微研究一下。我根据您上面描述的摘要构建了一个测试项目,并已成功使用 South 执行您所询问的迁移。在我们进入代码之前,这里有几点说明:

    • South 文档建议将架构迁移和数据迁移分开进行。我也照做了。

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

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

    • 在实际的数据迁移代码中,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
    

    为了篇幅,由于模型最终看起来总是一样的,我将只使用“电影”应用程序进行演示。

    电影/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'
    

    南方太棒了!

    好的标准免责声明:您正在处理实时数据。我在这里给了你工作代码,但请使用--db-dry-run 来测试你的架构。在尝试任何事情之前务必进行备份,并且通常要小心。

    兼容性声明

    我将保持我原来的消息不变,但 South 已将命令 manage.py startmigration 更改为 manage.py schemamigration

    【讨论】:

    • 非常感谢您为此付出的所有努力。如果我可以投票不止一次,我会的!我想我会听取您的建议并为模型命名,以避免将来出现名称冲突。这太棒了。
    • 几件事:1) startmigration 实际上被拆分为 schemamigrationdatamigration。后者不采用--auto--initial 标志,仅采用应用程序名称和迁移名称,并为您提供一个带有空forwardsbackwards 方法的迁移文件。 2) 根据 South 文档,您应该使用 raise RuntimeError("Cannot reverse this migration.") 而不是 print 'No Backwards'
    • 你为什么不用class Meta: abstract = True
    • @TStone,非常感谢您在这方面付出了如此多的努力,它真的帮助我了解了如何与南合作。
    • 我确实对这个解决方案有一些问题,我认为这主要是由于 django 1.2 和 south 0.7 的变化,请参阅下面最终对我有用的解决方案。
    【解决方案2】:

    我确实尝试过 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)
    

    显然有很多重叠,所以我想考虑共性 进入 general post 模型,只保留其他模型的差异 模型类。

    新设置:

    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
    

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

    接下来在 post1/models.py 中创建新的类 SimplePostExtPost 和 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】:

    Abstract Model

    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
    

    generic relation 也可能对你有用。

    【讨论】:

    • 如果我将父类定义为抽象模型,我可以跳过整个迁移过程吗?南方还会同步吗?
    • 抽象模型不同步。只有“孩子”。
    • O.. 对不起,我以前不使用南。
    • 我可以看到如何手动完成,但我已经有一个现有的用户群,并希望使用 south 来自动迁移并且不让他们丢失任何数据。
    • 如果你像这样使用抽象继承,我认为你根本不需要任何迁移。所以这是一个优点。不利的一面是您在数据库级别仍然存在重复,并且您无法一次查询所有 VideoFiles。
    【解决方案4】:

    我进行了类似的迁移,并选择分多个步骤进行。除了创建多个迁移之外,我还创建了向后迁移以在出现问题时提供后备。然后,我抓取了一些测试数据并前后迁移,直到我确定它在我向前迁移时正确输出。最后,我迁移了生产站点。

    【讨论】:

    • 您有迁移的示例吗?您是如何将数据从旧架构复制到新架构的?
    • @Andre 你看过南方的数据迁移文档了吗?这几乎就像正常使用 ORM 一样,除了您通过传递给您的向后/向前方法的“orm”参数来执行此操作(因此无论当前状态如何,您将始终拥有用于运行该迁移的模型的正确版本您的模型)。
    • 我确实看到并玩过它。我只是想知道我上面提到的步骤,首先重命名字段以免发生冲突是最简单的方法。
    猜你喜欢
    • 1970-01-01
    • 2011-06-29
    • 2015-11-02
    • 2012-10-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多