プログラミング言語やライブラリやフレームワークは、どんどん進化し新しいバージョンが提供されていく。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だ。
テスト用のプロジェクトを生成する。
テスト用のアプリケーションを作成する。
今回は、データベースにMySQLを使用する。MySQLの起動方法などは割愛するが、実施時はDockerで起動させた。そして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
マイグレーションを実行する。
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
再度、マイグレーションファイルを生成する。
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)),
],
),
]
再度マイグレーションを実行すると、正常にマイグレーションが行なわれる。
マイグレーションが完了し、必要なテーブルが作成されたから、テスト用のデータをDjangoのインタラクティブシェルから手動で登録してみる。
Djangoのインタラクティブシェルを起動したら、そのシェル上で以下のコードを実行し、データを登録する。
データを登録できた。ここまでは特に何も問題はない。
いよいよDjango 4系のLTSにアップグレードする。Django 4系のLTSは4.2だ。
アップグレードが完了したら、マイグレーションファイルを生成してみる。しかし、モデル定義は更新していないため、通常ではマイグレーションファイルは生成されないはずだ。
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.py
と app/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 |
%(class)s
のマイグレーションファイルでの扱いの違い細かな違いだが、この違いによって、マイグレーションファイルが新規に生成されていた。しかし、この違いはデータベース側には影響を与えないため、マイグレーションを実行しても実際には何もしない。