🚲

[Django]モデルを別のアプリに移動する方法

2021/12/22に公開

はじめに

これは 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 tablecreate 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_operationsmigrations.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 に移動元マイグレーションを追加
  • operationsstate_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が落ちる現象が発生して保留していました。

https://zenn.dev/ikemo/scraps/c6d0a97fb97208

原因は不明で、現在は落ちる現象は再現できないのですが、アプリケーション間でモデルを移動するのは手間がかかります。最終手段と考えておきたいです。

Discussion