Django migrations 初期化手順と関連の utility scripts

Django migrations 初期化手順と関連の utility scripts を公開します。

毎度の作業なので作業を極力自動化できるようスクリプト化しました。
どこまでスクリプトで解決するかは程度の問題でしょうが、このドキュメントはひとつの提案です。

概要

このドキュメントでやりたいことと想定する読者像

このドキュメントでやりたいこと

  • Django の migrations を DB 内のデータを損なわないままで初期化する手順を記載する
  • 関連の utility script を提供する

想定する読者像

以下のいずれかに該当する方

  • Django migrations 関連の以下の python manage.py コマンドについて、代表的なオプションを含めて使い方の知識がある方
    • python manage.py makemigrations
    • python manage.py migrate
    • python manage.py showmigration
  • 実務である程度の Django 開発経験がまあまああって、以下のようなことで苦慮しているという方
    • サーバ起動/ユニットテスト開始のオーバーヘッドが気になっている
    • 「migrations の初期化がしんどいな」と思っている/故に、ついつい後回しにしている
  • Django の migration の仕組みについて学習中だという方
  • チャレンジングな方
    • なんだか良くわからないけど、とりあえず読んでみたいという方
    • 理解が及ばないことについては、ウェブで調べたり生成AIなどに尋ねたりして自己解決できるという方
    • 「一読しただけではすべてを理解できない文書」と格闘するのが好きな方

なお、この文書に登場する python manage.py コマンドについては、最低限の解説を末尾に付与した。

解決したい課題と解決の方法、注意点

解決したい課題

Django アプリの開発が続くと、以下のような問題が起きがちである。

  • migrationsファイルが増えてくる
  • 起動/ユニットテスト開始時にオーバーヘッドが大きくなる

前者は大きな問題ではないが、後者は開発のパフォーマンスに大きく影響する。
この遅延が生じる主要因のひとつして、サーバ起動時に migration ファイル同士での依存関係の解決にリソースを取られるということが挙げられる。

そこで、この依存関係を解きほぐしたいのだが、そのいちばんの方法は、migrations ファイルをすべて破棄したうえで再度生成し、「初期化」してしまうことである。

「初期化」だけのことを考えるならば、Django Project 内の各アプリの migrations/ ファイルを一掃したうえで、新しいデータベースを参照するようにし、それから python manage.py makemigrations すれば解決する。

しかし、実運用しているサービスではそういうわけには行かない。
具体的には、以下のような点を考慮する必要がある。

  1. データベースに実質的に変更を生じさせてはならない
  2. migration 適用履歴か格納された django_migrations テーブルの状態を更新しなくてはならない

1番目の課題はは当然。
「作業が終わったらユーザデータがすべてなくなっていた」なんてことがあってはならない。

2番目の課題について補足すると、Djangoではマイグレーション適用履歴テーブルとして django_migrations テーブルがあるのだが、この適用状況情報を無視して作業すると不整合が生じてしまい、いろいろと面倒なことになってしまう。

解決の方法

そこで、この文書では、Django の migrations を DB 内のデータを損なわないままで初期化する手順を記載する。
また、関連の作業を容易に行えるような utility script を提供する。

なお、 migrations をすっきりさせる別の方法として、 python manage.py squashmigrations もある。
このコマンドにより、ひとつのアプリ内の複数の migrations ファイルをひとつにをまとめることができる。
とはいえこの方法でも限界があるので、今回は、それ以上にすっきりさせたい場合の手段としておすすめの方法を紹介している。

注意点

  • 本場環境では念の為バックアップを取ってから作業する
  • python manage.py migrate コマンドはすべて --fake が入ってないと本当にDBのスキーマが変更されてデータが消えてしまうこともあるので注意
  • この手順に記載のとおりに作業したことによるいかなる結果にも責任は負えないので、以下のことなど、このドキュメントを読んでいる形の自己責任で行ってほしい
    • 作業体像を把握する
    • 各作業の詳細を把握する
    • 提供されたスクリプトが所望の動作をするかどうかを確認する

なお、本記事内容およびスクリプトについて、開発の現場での利用にいっさいの制限を設けないので好きに使ってほしい。

実作業

作業のおおまかな流れ

作業のおおまかな流れは以下のとおり。

  1. 本番環境で以下をしておく
    1. 最新の migrations があてられた状態にしておく
  2. local で migrations を初期化する
    1. 状況確認
    2. django_migrations テーブルの内容をリセット(--fake)
    3. migrations ファイルを削除
    4. マイグレーションファイルを新規作成
    5. マイグレーションを適用(--fake)
    6. 動作確認
    7. git で push
  3. 本番環境で migrations を初期化する
    1. 状況確認
    2. django_migrations テーブルの内容をリセット(--fake)
    3. git で pull
    4. マイグレーションを適用(--fake)
    5. Django app の再起動
    6. 動作確認

以下、詳細に見ていく。

1. 本番環境で事前準備をする

本番環境では最新の migrations が適用しておく。
(失念してしまった場合の対処方法は後述)

2. local で migrations を初期化する

以下の手順で、 local で migrations を初期化する。

2-1. 状況確認

手動でやるなら、ひとつひとつ app の dir 名をテキストエディタにでも転記していく。

Windows 環境下でスクリプトで済ますならば、以下のスクリプトを実行して 1 を選択でOK。

.\.utils\reset_migrations.ps1 

2-2. django_migrations テーブルの内容をリセット(--fake)

django_migrations の内容をリセットする。

手動でやるなら、「2-1. 状況確認」で取得した dir 名ひとつひひとつに対して、以下を実行していく。

python manage.py migrate dir_name zero --fake

Windows 環境下でスクリプトで済ますならば、以下のスクリプトを実行して 2 を選択でOK。

.\.utils\reset_migrations.ps1 

ここで気になるなら python manage.py showmigration で migrations の状態を確認

2-3. migrations ファイルを削除

各 app 内の migrations/ フォルダ内のファイルを削除

手動でやるなら、 1. で取得した dir 名ひとつひひとつに対して、 /migrations/ フォルダ内のファイルを削除していく。

Windows 環境下でスクリプトで済ますならば、以下のスクリプトを実行して 3 を選択でOK。

.\.utils\reset_migrations.ps1 

ここで気になるなら python manage.py showmigration で migrations の状態を確認

2-4. マイグレーションファイルを新規作成

マイグレーションファイルを作成する。

python manage.py makemigrations

ここで気になるなら python manage.py showmigration で migrations の状態を確認

2-5. マイグレーションを適用(--fake)

マイグレーションを適用する。

python manage.py migrate --fake

ここで気になるなら python manage.py showmigration で migrations の状態を確認

2-6. UI からの動作確認

2-6-1. UI からの動作確認

python manage.py runserver したあとブラウザで動作確認してみる。

2-6-2. unit test を実行
python manage.py test

2-7. git で push

git add, commit, push

git add .
git commit -m "migrations reset"
git push

3. 本番環境で migrations を初期化する

3-1. 状況確認

手動でやるなら、ひとつひとつ app の dir 名をテキストエディタにでも転記していく。
のであるが、 Windows ローカルでこれは済んでいるであろう。

Linux 環境下でスクリプトで済ますならば、以下のスクリプトを実行して 1 を選択でOK。

.utils/manage_migrations.sh

3-2. django_migrations テーブルの内容をリセット(--fake)

注意!!
もしも、本番環境で最新の migrations が適用されていない場合は?
その場合は、まず、この作業の前に、いったん git pull してしまう。
そして、適当なコミットに遡って最新の migrations を適用しておく。

手動でやるなら、 1. で取得した dir 名ひとつひひとつに対して、以下を実行していく。

python manage.py migrate dir_name zero --fake

Linux 環境下でスクリプトで済ますならば、以下のスクリプトを実行して 2 を選択でOK。

.utils/manage_migrations.sh

ここで気になるなら python manage.py showmigration で migrations の状態を確認

3-3. git pull

git pull して、local での migrations の状態を本番環境に反映させる。

git pull

3-4. マイグレーションを適用(--fake)

マイグレーションを適用する。

python manage.py migrate --fake

ここで気になるなら python manage.py check でエラーがでないか確認。

3-5. Django app の再起動

Django app を再起動してみる。

sudo systemctl restart [登録したサービス名]

3-6. 動作確認

3-6-1. UI からの動作確認

ブラウザで動作確認してみる。

3-6-2. unit test を実行

ダメ押しに、 unit test を実行して問題が生じないことを確認。

python manage.py test

script 紹介

reset_migrations.ps1

Windows ローカル環境用。
上記の解説では、以下のディレクトリに置いたという仮定で説明した。

.utils/reset_migrations.ps1

# Manage-Migrations.ps1

# Function to get relevant directories
function Get-MigrationDirs
{
    $projectRoot = Split-Path -Parent $PSScriptRoot
    Get-ChildItem -Path $projectRoot -Directory |
            Where-Object { $_.Name -notin @("venv", ".venv") } |
            Where-Object { Test-Path (Join-Path $_.FullName "migrations") }
}

# Function to set migrations to zero
function Set-MigrationsToZero
{
    param (
        [Parameter(Mandatory = $true)]
        [System.IO.DirectoryInfo[]]$Directories
    )
    foreach ($dir in $Directories)
    {
        $appName = $dir.Name
        Write-Host "Setting migrations to zero for app: $appName" -ForegroundColor Yellow
        $result = python manage.py migrate $appName zero --fake
        Write-Host $result -ForegroundColor Cyan
    }
}

# Function to remove migration files
function Remove-MigrationFiles
{
    param (
        [Parameter(Mandatory = $true)]
        [System.IO.DirectoryInfo[]]$Directories
    )
    foreach ($dir in $Directories)
    {
        $migrationPath = Join-Path $dir.FullName "migrations"
        Get-ChildItem -Path $migrationPath -File |
                Where-Object { $_.Name -ne "__init__.py" } |
                ForEach-Object {
                    Remove-Item $_.FullName -Force
                    Write-Host "  Deleted: $( $_.FullName )" -ForegroundColor Yellow
                }
    }
}

# User choice prompt
$userChoice = Read-Host @"
Choose an action:
1. List migration directories
2. Set migrations to zero (fake)
3. Reset migrations (delete and recreate)
Enter 1, 2, or 3
"@

# Display selection result
Write-Host "Your choice: $userChoice" -ForegroundColor Cyan

# Display message based on selection
switch ($userChoice)
{
    "1" {
        Write-Host "Listing migration directories:" -ForegroundColor Yellow
        $migrationDirs = Get-MigrationDirs
        foreach ($dir in $migrationDirs)
        {
            Write-Host "  - $( $dir.Name )" -ForegroundColor Green
        }
    }
    "2" {
        Write-Host "Setting migrations to zero:" -ForegroundColor Yellow
        $migrationDirs = Get-MigrationDirs
        $confirm = Read-Host "Are you sure you want to set all migrations to zero? (y/n)"
        if ($confirm -eq "y")
        {
            Set-MigrationsToZero -Directories $migrationDirs
            Write-Host "All migrations have been set to zero." -ForegroundColor Green
        }
        else
        {
            Write-Host "Operation cancelled." -ForegroundColor Red
        }
    }
    "3" {
        Write-Host "Removing all migration files:" -ForegroundColor Yellow
        $migrationDirs = Get-MigrationDirs
        $confirm = Read-Host "Are you sure you want to remove all migration files? (y/n)"
        if ($confirm -eq "y")
        {
            Remove-MigrationFiles -Directories $migrationDirs
            Write-Host "All migration files have been removed." -ForegroundColor Green
        }
        else
        {
            Write-Host "Operation cancelled." -ForegroundColor Red
        }
    }
    default {
        Write-Host "Invalid choice. Please enter 1, 2, or 3." -ForegroundColor Red
    }
}

Write-Host "Script execution completed." -ForegroundColor Green

manage_migrations.sh

Linux 環境用。
上記の解説では、以下のディレクトリに置いたという仮定で説明した。

.utils/manage_migrations.sh

#!/bin/bash

# Function to get relevant directories (sorted alphabetically)
get_migration_dirs() {
    local project_root
    project_root=$(dirname "$(dirname "$0")")
    find "$project_root" -maxdepth 1 -type d | while read -r dir; do
        if [[ $(basename "$dir") != "venv" && $(basename "$dir") != ".venv" && -d "$dir/migrations" ]]; then
            basename "$dir"
        fi
    done | sort
}

# Function to set migrations to zero
set_migrations_to_zero() {
    while read -r app_name; do
        echo -e "\e[33mSetting migrations to zero for app: $app_name\e[0m"
        python manage.py migrate "$app_name" zero --fake
    done
}

# Function to remove migration files
remove_migration_files() {
    local project_root
    project_root=$(dirname "$(dirname "$0")")
    while read -r app_name; do
        local migration_path="$project_root/$app_name/migrations"
        find "$migration_path" -type f ! -name "__init__.py" -delete -print | while read -r file; do
            echo -e "\e[33m  Deleted: $file\e[0m"
        done
    done
}

# User choice prompt
echo "Choose an action:"
echo "1. List migration directories"
echo "2. Set migrations to zero (fake)"
echo "3. Reset migrations (delete and recreate)"
read -rp "Enter 1, 2, or 3: " user_choice

echo -e "\e[36mYour choice: $user_choice\e[0m"

# Display message based on selection
case $user_choice in
    1)
        echo -e "\e[33mListing migration directories:\e[0m"
        get_migration_dirs | while read -r dir; do
            echo -e "\e[32m  - $dir\e[0m"
        done
        ;;
    2)
        echo -e "\e[33mSetting migrations to zero:\e[0m"
        read -rp "Are you sure you want to set all migrations to zero? (y/n): " confirm
        if [[ $confirm == "y" ]]; then
            get_migration_dirs | set_migrations_to_zero
            echo -e "\e[32mAll migrations have been set to zero.\e[0m"
        else
            echo -e "\e[31mOperation cancelled.\e[0m"
        fi
        ;;
    3)
        echo -e "\e[33mRemoving all migration files:\e[0m"
        read -rp "Are you sure you want to remove all migration files? (y/n): " confirm
        if [[ $confirm == "y" ]]; then
            get_migration_dirs | remove_migration_files
            echo -e "\e[32mAll migration files have been removed.\e[0m"
        else
            echo -e "\e[31mOperation cancelled.\e[0m"
        fi
        ;;
    *)
        echo -e "\e[31mInvalid choice. Please enter 1, 2, or 3.\e[0m"
        ;;
esac

echo -e "\e[32mScript execution completed.\e[0m"

なお、 manage_migrations.sh については、配置後に以下のコマンドを実行し、実行可能にする必要がある。

chmod +x .utils/manage_migrations.sh

python manage.py commandメモ:

python manage.py makemigrations

マイグレーションファイルを作成する(必要な場合のみ)

「必要な場合」とは、models.Model クラスを継承してつくられたモデルクラスへの変更が migrations ファイルに反映されていない場合。
なので、migrations ファイルがひとつもない状態で python manage.py すれば、 migrations ファイルが作られることになる。
また、そのとき「数値4桁のシーケンス番号が最初につく」というネーミングルール。
例: 0001_initial.py

python manage.py migrate

マイグレーションを適用する

「マイグレーションを適用する」とは、各アプリの migratinos/ ディレクトリ内に未適応のマイグレーションファイルがある場合、それを適用すること。
これにより、モデルに対応したテーブルが作成されたり、テーブルのカラムが変更されたりする。

オプションで を指定することで、特定のアプリのマイグレーションのみを適用することもできる。
そのとき、 migrations ファイルを指定することで、そのファイルまでのマイグレーションのみを適用することもできる。

例: some_app/migrations に以下の4つのファイルがあったとする。

  • 0001_initial.py
  • 0002_add_column.py
  • 0003_remove_column.py
  • 0004_add_table.py

そして、以下のコマンドを実行したとする。

python manage.py migrate some_app 0002_add_column

このとき、DBは、0001_initial.py と 0002_add_column.py が適用された状態になる。

python manage.py migrate some_app zero とすると、some_app のマイグレーションをすべて取り消すことができる。
(0001_initial.py が適用される前の状態になる)

なお、各 app でどこまでのマイグレーションが適用されているかは、表に出ない django_migrations テーブルに記録されている。
python manage.py showmigration その内容を確認できる。

python manage.py migrate コマンドでは、 --fake オプションをつけることもできる。
--fake オプションをつけると、マイグレーションファイルの内容を適用せず、 migration 履歴のテーブルのみを更新する。

python manage.py showmigration

マイグレーションの状態を確認する

たとえば some_app の migrations ファイルが前記のようであって適用された migration が 0002_add_column までだった場合は、以下のように表示される。

python manage.py showmigration some_app
some_app
 [X] 0001_initial
 [X] 0002_add_column
 [ ] 0003_remove_column
 [ ] 0004_add_table

[X] がついているものは適用されているマイグレーション、 [ ] がついているものは未適用のマイグレーションを示す。

公開日時: 2024/07/23 12:00