southを使用して、継承を使用してDjangoモデルをリファクタリングする
-
05-07-2019 - |
質問
Django south で次の移行が可能かどうか疑問に思っていました。
>前:
現在2つのアプリがあり、1つはtv、もう1つは映画と呼ばれ、それぞれ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)
後:
2つのvideofileオブジェクトは非常に似ているため、重複を取り除き、一般的なVideoFileクラスを含むmediaという別のアプリで新しいモデルを作成し、継承を使用してそれを拡張します:
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でこれを達成し、既存のデータを維持するにはどうすればよいですか?
これら3つのアプリはすべて南の移行によって既に管理されています。南のドキュメントによると、スキーマとデータの移行を組み合わせるのは悪い習慣であり、数ステップで実行することをお勧めします。
このような個別の移行を使用して実行できると思います(media.VideoFileが既に作成されていると仮定)
- 新しいmedia.VideoFileモデルに移動するtv.VideoFileおよびmovies.VideoFileのすべてのフィールドの名前を変更するスキーマ移行。old_name、old_sizeなどのようなものになります
- media.VideoFileから継承するためのtv.VideoFileおよびmovies.VideoFileへのスキーマ移行
- old_nameを名前に、old_sizeをサイズに、などにコピーするデータ移行
- 古いフィールドを削除するためのスキームの移行
すべての作業を行う前に、それが機能すると思いますか?より良い方法はありますか?
興味がある場合、プロジェクトはここでホストされます: http://code.google。 com / p / medianav /
解決
Django / Southの新しいバージョンとの互換性に関する注意事項については、以下のPaulの回答をご覧ください。
これは興味深い問題のように思えたので、私は南の大ファンになりつつあるので、少し調べてみることにしました。上記で説明した内容の要約に基づいてテストプロジェクトを作成し、Southを使用して、求めている移行を実行しました。コードに着手する前の注意事項を以下に示します。
-
Southのドキュメントでは、スキーマの移行とデータの移行を別々に行うことを推奨しています。私はこれに追随しました。
-
バックエンドでは、Djangoは継承モデルにOneToOneフィールドを自動的に作成することにより、継承テーブルを表します
-
これを理解しているため、Southの移行ではOneToOneフィールドを手動で適切に処理する必要がありますが、これを試してみると、South(またはDjango自体)が同じ名前の複数の継承テーブルにファイルされたOneToOneを作成できないようです。このため、movies / tvアプリの各子テーブルの名前をそれぞれのアプリ(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
スペースのために、そしてモデルは常に同じように見えるため、「ムービー」アプリでのみデモンストレーションします。
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'
南は素晴らしい!
OK標準免責事項:ライブデータを扱っています。ここで作業コードを提供しましたが、-db-dry-run
を使用してスキーマをテストしてください。何かを試す前に常にバックアップを作成し、一般的に注意してください。
互換性に関する通知
元のメッセージはそのまま残しますが、Southはコマンド manage.py startmigration
を manage.py schemamigration
に変更しました。
他のヒント
私はT Stoneによって概説された解決策を試してみましたが、それは素晴らしいスターターであり、物事がどのように行われるべきかを説明している間、いくつかの問題に遭遇しました。
ほとんどの場合、親クラスのテーブルエントリを作成する必要はもうありません。つまり、必要ありません
new_movie.videofile_ptr = orm['media.VideoFile'].objects.create()
もう。 Djangoはこれを自動的に行います(null以外のフィールドがある場合、上記は機能せず、データベースエラーが発生しました)。
おそらくdjangoとsouthの変更が原因だと思います。ここでは、ubango tu 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
(シェルプロンプトを表すのに $
を使用しているので、入力しないでください。)
次に、post1 / models.pyに新しいクラス SimplePost および ExtPost を作成します それぞれpost2 / models.py(残りのクラスはまだ削除しないでください)。 次に、これら2つのスキーマ移行も作成します。
$./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から削除し、スキーマ移行を作成してテーブルを新しい状態に更新します。
<*>これで完了です!うまくいけば、それがすべて機能し、モデルをリファクタリングしたことになります。
同様の移行を行い、複数のステップで移行することを選択しました。複数の移行を作成することに加えて、逆方向の移行も作成して、問題が発生した場合にフォールバックを提供します。次に、いくつかのテストデータを取得し、前方に移行したときにデータが正しく出力されることが確認されるまで、前後に移行しました。最後に、本番サイトを移行しました。