« ^ »

Django3からDjango4へ移行する時にマイグレーションファイルが生成されるかを確認する

所要時間: 約 6分

プログラミング言語やライブラリやフレームワークは、どんどん進化し新しいバージョンが提供されていく。Webに関わるようなそれらは特にそのスピードが速い。システム開発の現場では、日々のそれらの更新に追従していく事を求められる。

私はDjangoというWebフレームワークをよく使用する。DjangoはPython製のWebフレームワークで人気がある。そして2023年04月03日、Django 4.2がリリースされた。Django4.2は long-term support release(以降、LTS) と呼ばれ、これは長期間サポートする事を公式から明言される。

リリースされたバージョンにはサポート期間があり、その期間が終了すると、バグ修正や、セキュリティ的に重大な問題しか対応されないようになる。それではシステムの運用に支障が出るため、サポート期間が切れる前に、新しいバージョンにアップグレードする。サポート期間が短いという事は、すぐにアップグレードしなければならなくなる。逆に長期間サポートされるバージョンは、長期間アップグレードしなくても良くなる。そのためLTSがリリースされるごとに、最新のLTSにアップグレードするという方針を取っているプロジェクトは多い。

私も同様の方針を取っており、Django 3系のLTSからDjango 4系のLTSへ、移行する事にした。Djangoをアップグレードし、マイグレーションファイルを python manage.py makemigrations で生成する。コードの修正は行っていないから、マイグレーションファイルは生成されないはずだった。しかし、実際には新しいマイグレーションファイルが生成された。今回はこの原因を調べる事にした。調査方法は色々あると思うが、プログラムやシステムが小さければ小さいほど調査は楽になる事が多い。そこで小さなDjangoプロジェクトをDjango 3系のLTSで新規に生成し、その後Django 4系のLTSへアップグレードしマイグレーションファイルを生成する事で違いを確認する。

まずDjango 3系のLTSをインストールする。Django 3系のLTSは3.2だ。

pip install django==3.2
Django 3系のLTSをインストールする

テスト用のプロジェクトを生成する。

django-admin startproject testing .
テスト用のプロジェクトtestingを生成する

テスト用のアプリケーションを作成する。

python manage.py startapp app
テスト用のアプリケーションappを生成する

今回は、データベースにMySQLを使用する。MySQLの起動方法などは割愛するが、実施時はDockerで起動させた。そしてMySQLにデータベースを作成する。

CREATE DATABASE testing;
mysqlコマンドでMySQLに接続しデータベースを作成する

設定ファイルを書き換え、作成したアプリケーションをINSTALLED_APPSに追加する。またデータベースの接続情報を設定する。

"""
Django settings for testing project.

Generated by 'django-admin startproject' using Django 3.2.

For more information on this file, see
https://docs.djangoproject.com/en/3.2/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.2/ref/settings/
"""

from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '??????????????????????????????????????????????????????????????????'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []


# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    "app",
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'testing.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'testing.wsgi.application'


# Database
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': "testing",
        "HOST": "127.0.0.1",
        "PORT": "3306",
        "USER": "root",
    }
}


# Password validation
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/3.2/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.2/howto/static-files/

STATIC_URL = '/static/'

# Default primary key field type
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

testing/settings.py

マイグレーションを実行する。

python manage.py migrate
マイグレーションを実行する
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying sessions.0001_initial... OK

マイグレーションは正常に終了した。

マイグレーションファイルを生成するにはモデル定義が必要だ。簡易なモデル Foo を定義し、userフィールドにauth_userテーブルのidに対する外部キー制約を追加した。related_nameは通常指定の必要はないが、ここではプレースホルダーのスタイルとして指定した。

from django.db import models

# Create your models here.
class Foo(models.Model):
    user = models.ForeignKey('auth.user', on_delete=models.CASCADE, related_name='foo_%(class)s_set')

app/models.py

再度、マイグレーションファイルを生成する。

python manage.py makemigrations
マイグレーションファイルを生成する
Migrations for 'app':
  app/migrations/0001_initial.py
    - Create model Foo

アプリケーションappに最初のマイグレーションファイルが生成された。

# Generated by Django 3.2 on 2023-05-27 05:54

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    initial = True

    dependencies = [
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
    ]

    operations = [
        migrations.CreateModel(
            name='Foo',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='foo_foo_set', to=settings.AUTH_USER_MODEL)),
            ],
        ),
    ]

再度マイグレーションを実行すると、正常にマイグレーションが行なわれる。

python manage.py migrate
マイグレーションを実行する

マイグレーションが完了し、必要なテーブルが作成されたから、テスト用のデータをDjangoのインタラクティブシェルから手動で登録してみる。

python manage.py shell
Djangoのインタラクティブシェルを起動する

Djangoのインタラクティブシェルを起動したら、そのシェル上で以下のコードを実行し、データを登録する。

from django.contrib.auth.models import User
from app.models import Foo

u = User()
u.username = "testing"
u.set_password("foo")
u.save()

f = Foo()
f.user = u
f.save()
ユーザーを追加し、Fooクラスが管理しているテーブルにデータを登録する

データを登録できた。ここまでは特に何も問題はない。

いよいよDjango 4系のLTSにアップグレードする。Django 4系のLTSは4.2だ。

pip install -U django==4.2
Django 4系のLTSにアップグレードする

アップグレードが完了したら、マイグレーションファイルを生成してみる。しかし、モデル定義は更新していないため、通常ではマイグレーションファイルは生成されないはずだ。

python manage.py makemigrations
マイグレーションファイルを生成する
Migrations for 'app':
  app/migrations/0002_alter_foo_user.py
    - Alter foo user

するとマイグレーションファイルが生成された。このマイグレーションファイルの中身を確認する。

# Generated by Django 4.2.1 on 2023-05-27 06:08

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):
    dependencies = [
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
        ("app", "0001_initial"),
    ]

    operations = [
        migrations.AlterField(
            model_name="foo",
            name="user",
            field=models.ForeignKey(
                on_delete=django.db.models.deletion.CASCADE,
                related_name="foo_%(class)s_set",
                to=settings.AUTH_USER_MODEL,
            ),
        ),
    ]

app/migrations/0002_alter_foo_user.py

app/migrations/0001_initial.pyapp/migrations/0002_alter_foo_user.py を見比べながら違う所を探す。 0001_initial.py はテーブル定義であり 0002_alter_foo_user.py は変更であるため、そもそも記述は異なるが、値が違うような所を探していく。するとrelated_nameの設定が異なる事がわかった。 0001_initial.py はプレースホルダー( %(class)s の部分)が展開された状態でマイグレーションファイルに記述されているのに対し、 0002_alter_foo_user.py ではプレースホルダーが展開されず、そのまま指定されていた。

バージョン%(class)s のマイグレーションファイルでの扱いFooクラスに定義されている場合の例
Django3系展開された状態でマイグレーションファイルに保持されるfoo
Django4系展開されずプレースホルダーがそのままマイグレーションファイルに保持される%(class)s
Django3系とDjango4系での %(class)s のマイグレーションファイルでの扱いの違い

細かな違いだが、この違いによって、マイグレーションファイルが新規に生成されていた。しかし、この違いはデータベース側には影響を与えないため、マイグレーションを実行しても実際には何もしない。