【Django x MySQL】マイグレーション時にSQLを自動適用する方法（テンポラリーテーブルを使用）

# 概要
* とあるレコードを、Djangoマイグレーションと同時に追加したい仕様がありました。というのも、それらのレコードがないとユーザーのアクセス権限上アプリを動かすことができず、また、手動でSQL実行するには伝達に手間がかかるためです。
* ということで、マイグレートと同時にSQL実行させる実装をしたので、こちらに記載します。

# 前提・実行方法
* Djangoのマイグレーション時にSQLを実行するためには、マイグレーションファイル内でカスタムのSQL操作を定義する、という方法があります。DjangoのORMの組み込み機能だけで難しいっぽいです。
* 今回は、データベースエンジンとしてMySQLを利用している前提です。
    * Postgresでは`UPSERT（INSERT ... ON CONFLICT）`とか`ON CONFLICT (id) DO UPDATE`を使えばできると思います。
* `id`の重複を回避したいので、仮に同じ`id`が存在する場合は上書きするようにしています。なので、今回挿入されるレコードはかなり絶対的なデータとして扱われるものを前提としています。
* 手順としては、以下の流れになります。
    * 一時テーブル（テンポラリーテーブル）を作成
    * 新しいデータを使用して置き換え（既存のレコードを削除）
    * 一時テーブルのデータを本テーブルに挿入
    * 一時テーブルを削除

### テンポラリーテーブルとは？
* テンポラリーテーブルとは、一時的なデータを格納するために使用されるデータベース内のテーブルのこと。
    * 一時的な作業領域として使用されるので、アプリケーションのパフォーマンスを向上させることにも使われるようです。
* `CREATE TEMPORARY TABLE`ステートメントは、一時的なテーブルをデータベース内に作成するためのSQLコマンドです。一時テーブルはセッションの範囲内でのみ存在し、セッションが終了すると自動的に削除されます。永続性はないので、データベースの再起動後にはもちろん存在しません。
* 通常のテーブルと同様に、データの挿入、更新、削除などのSQLクエリが実行可能です。
* 公式：
    * [13.1.20.2 CREATE TEMPORARY TABLE ステートメント](https://dev.mysql.com/doc/refman/8.0/ja/create-temporary-table.html)

## サンプルコード
* 以下にサンプルコードを貼り付けましたので、ご参考ください。


```py
# Generated by Django 3.0.5 on 2023-06-02 15:30

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('sample', '0001_initial'),
    ]

    operations = [
        migrations.RunSQL(
            """
            -- Create temporary table for auth_group
            CREATE TEMPORARY TABLE temp_auth_group (
                id INT PRIMARY KEY,
                name VARCHAR(255)
            );

            -- Insert data into temporary table for auth_group
            INSERT INTO temp_auth_group (id, name)
            VALUES
              (1, 'group1'),
              (2, 'group2'),
              (3, 'group3');

            -- Update existing records in auth_group using temporary table data
            UPDATE auth_group
            INNER JOIN temp_auth_group ON auth_group.id = temp_auth_group.id
            SET auth_group.name = temp_auth_group.name;

            -- Insert new records into auth_group from temporary table
            INSERT INTO auth_group (id, name)
            SELECT id, name FROM temp_auth_group
            WHERE id NOT IN (SELECT id FROM auth_group);

            -- Drop temporary table for auth_group
            DROP TEMPORARY TABLE IF EXISTS temp_auth_group;
            """
        ),
        migrations.RunSQL(
            """
            -- Create temporary table for path
            CREATE TEMPORARY TABLE temp_path (
                path_id INT PRIMARY KEY,
                path VARCHAR(255)
            );

            -- Insert data into temporary table for path
            INSERT INTO temp_path (path_id, path)
            VALUES
              (1, '/sample1/'),
              (2, '/sample2/'),
              (3, '/sample3/');

            -- Update existing records in path using temporary table data
            UPDATE path
            INNER JOIN temp_path ON path.path_id = temp_path.path_id
            SET path.path = temp_path.path;

            -- Insert new records into path from temporary table
            INSERT INTO path (path_id, path)
            SELECT path_id, path FROM temp_path
            WHERE path_id NOT IN (SELECT path_id FROM path);

            -- Drop temporary table for path
            DROP TEMPORARY TABLE IF EXISTS temp_path;
            """
        ),
        migrations.RunSQL(
            """
            -- Create temporary table for path_x_group
            CREATE TEMPORARY TABLE temp_path_x_group (
                path_x_group_id INT PRIMARY KEY,
                group_id INT,
                path_id INT
            );

            -- Insert data into temporary table for path_x_group
            INSERT INTO temp_path_x_group (path_x_group_id, group_id, path_id)
            VALUES
              (1, 1, 1),
              (2, 1, 2),
              (3, 2, 1),
              (4, 2, 2),
              (5, 2, 3),
              (6, 3, 2),
              (7, 3, 3);

            -- Update existing records in path_x_group using temporary table data
            UPDATE path_x_group
            INNER JOIN temp_path_x_group ON path_x_group.path_x_group_id = temp_path_x_group.path_x_group_id
            SET path_x_group.group_id = temp_path_x_group.group_id, path_x_group.path_id = temp_path_x_group.path_id;

            -- Insert new records into path_x_group from temporary table
            INSERT INTO path_x_group (path_x_group_id, group_id, path_id)
            SELECT path_x_group_id, group_id, path_id FROM temp_path_x_group
            WHERE path_x_group_id NOT IN (SELECT path_x_group_id FROM path_x_group);

            -- Drop temporary table for path_x_group
            DROP TEMPORARY TABLE IF EXISTS temp_path_x_group;
            """
        ),
    ]
```



## 外部キー制約エラーが発生した場合
* 外部キー制約に違反する操作がある場合、以下のエラーが返ってきます。

```
django.db.utils.IntegrityError: (1451, 'Cannot delete or update a parent row: a foreign key constraint fails (...
```

* この場合、関連するテーブルのレコードをあらかじめ削除しておく必要があります。今回の実装では、他の環境で外部キー制約が発生することは考えづらく、自分の環境でのみ発生することだったので手動で削除対応としました。