[Django]モデルを別のアプリに移動する方法
はじめに
これは Django Advent Calendar 2021 12日目の記事です(空いていたので飛び入りで)。
Django では、アプリケーションという単位で機能を分割できます。モデルを作成した場合通常はそのアプリケーションから動かすことはないのですが、稀に、モデルを別のアプリケーションに動かしたい場合があります。
この記事では、あるアプリケーションのモデルを別のアプリケーションに移動する方法について書きます。正しく理解して行うと問題ありませんが、間違うとデータが削除されてしまうので注意してください。
単なるモデルの移動ではダメな理由
単にモデルを移動したいだけなら次の手順でできます。
- 移動元のアプリケーションからモデルを削除
- 移動先のアプリケーションに同じモデルを作成
これでマイグレーションファイルを作成するだけで、別のアプリケーションにモデルが移動できます(もちろん、モデルを参照するビューなどの変更は必要です)。
簡単でしょう?
しかしこの方法には致命的な問題があります。データが全て削除されます。なぜなら、先の方法をSQLで表すと、次のようになるからです。
drop table foo;
create table foo (
...
);
要はテーブルを削除して、同じテーブルを作り直します。当然の結果として、データが削除されます。
正確にはテーブル名にアプリケーション名が付くため、削除するテーブルと作成するテーブルは別のテーブル名(例: myapp_foo
になりますが、モデルの Meta.db_table
パラメータを使って同じテーブル名にしても結果は同じです。
もちろんデータの移行を行えば移動可能ですが、アプリケーション間のモデルの移動のためにデータの移行が必要なのは不便です。
Djangoでは現在この機能はサポートされていません(Issue #24686)。しかし、 SeparateDatabaseAndState
という機能を使うことで比較的簡単に移行することが可能です。
SeparateDatabaseAndStateとは
SeparateDatabaseAndState
とは、Django のマイグレーションの操作で、その名の通り、データベース操作と状態操作を分離する機能です。
データベース操作は drop table
、 create table
のような、SQLを用いた操作です。一方で状態操作は、Djangoが内部で管理している、モデルの追加削除と言った状態の変更を指します。
通常はこの2つは一致するのですが、 SeparateDatabaseAndState では、あえてこの二つを分離します。
これを用いてモデルを別のアプリケーションに移動するには、次のような操作を行います。これによって、Djangoの状態変更しつつ、データベースに変更を加えないことが可能です。
- 移動元のアプリケーションからモデルを削除したことにする(データベースは何も変更しない)
- 移動先のアプリケーションに同じモデルを作成したことにする(データベースは何も変更しない)
実際の例
モデルを別のアプリケーションに移動する実際の方法を書いてみます。次のような Event
モデルを考えます(プライベートで作っているものです)。このアプリケーションを life
アプリから task
アプリに移動します。
from django.db import models
from django.db.models import CharField, DateField
class Event(models.Model):
name = CharField(max_length=50, verbose_name="名前")
date = DateField(verbose_name="日付")
class Meta:
verbose_name = verbose_name_plural = "イベント"
1. 移動元アプリケーション側の対応
まずは移動元アプリケーション側の対応です。まずは空のマイグレーションファイルを作成します。
python manage.py makemigrations life --empty --name delete_state
すると、次のようなマイグレーションファイルが作成されます。
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('life', '0022_auto_20210203_2133'),
]
operations = [
]
これを次のように変えます。
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("life", "0022_auto_20210203_2133"),
]
state_operations = [
migrations.DeleteModel("Event"),
]
operations = [
migrations.SeparateDatabaseAndState(state_operations=state_operations)
]
次のような操作を行うマイグレーションファイルになっています。複数のモデルを同時に移動する際には state_operations
に migrations.DeleteModel()
を追加してください。
-
database_operations
: データベース操作。空なので何もしない。 -
state_operations
: 状態操作。Event
モデルを削除したことにする。
このマイグレーションファイルは本当にデータベースを何も変更しないのでしょうか?それを確認するために、次のコマンドを打ちます。
python manage.py sqlmigrate life 0023
すると、次のSQLが表示されます。トランザクションを開始してコミットするだけです。本当に何もしません。
BEGIN;
--
-- Custom state/database change combination
--
COMMIT;
これで、移動元アプリケーションの対応は終わりです。
2. 移動先アプリケーションの対応
次に、移動先アプリケーションの対応を行います。まず、移動元のアプリケーションからモデルを削除し、新しいアプリケーションにモデルを作成します。
from django.db import models
from django.db.models import CharField, DateField
class Event(models.Model):
name = CharField(max_length=50, verbose_name="名前")
date = DateField(verbose_name="日付")
class Meta:
verbose_name = verbose_name_plural = "イベント"
db_table = "life_event"
ポイントは db_table = "life_event"
です。これは、テーブル名を変更しないために行っています。 この時点ではテーブル名を変更せず、必要なら後でリネームすることを推奨します。 互換性を保持するために、移動元のモデルには次のようにインポートしておくのもいいでしょう。
# noqaはflake8の警告を無視するコメント
from apps.task.models import Event # noqa
そして、マイグレーションファイルを生成します。
python manage.py makemigrations
次のようなマイグレーションファイルが作成されました。しかしこのまま適用してはいけません。
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('task', '0002_auto_20210203_2301'),
]
operations = [
migrations.CreateModel(
name='Event',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50, verbose_name='名前')),
('date', models.DateField(verbose_name='日付')),
],
options={
'verbose_name': 'イベント',
'verbose_name_plural': 'イベント',
'db_table': 'life_event',
},
),
]
次のように変更します。
-
dependencies
に移動元マイグレーションを追加 -
operations
をstate_operations
にリネーム -
operations
を新しく追加
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('task', '0002_auto_20210203_2301'),
('life', '0023_delete_state'),
]
state_operations = [
migrations.CreateModel(
name='Event',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50, verbose_name='名前')),
('date', models.DateField(verbose_name='日付')),
],
options={
'verbose_name': 'イベント',
'verbose_name_plural': 'イベント',
'db_table': 'life_event',
},
),
]
operations = [
migrations.SeparateDatabaseAndState(state_operations=state_operations)
]
このマイグレーションファイルも、データベースには何も行いません。sqlmigrateの結果は次のようになります。
BEGIN;
--
-- Custom state/database change combination
--
COMMIT;
3. 状態が正しいことを確認する
最後に、状態が正しいことを確認します。次のコマンドを打ってください。
No changes detected
と出れば問題ありません。
python manage.py makemigrations --dry-run
おわりに
この記事は2月に書いたのですが、なぜかpytestが落ちる現象が発生して保留していました。
原因は不明で、現在は落ちる現象は再現できないのですが、アプリケーション間でモデルを移動するのは手間がかかります。最終手段と考えておきたいです。
Discussion