Django 4.2から5.2へ(前編)リリースノートに載っていない落とし穴

皆さま、こんにちは。グローバルウェイのKです。
既存の業務システムの保守運用に携わっており、今回、稼働中のシステムでDjangoを4.2系から5.2系へバージョンアップしました。その知見を、前編・後編の2回に分けてお伝えします。
バージョンアップと聞くと、番号を書き換えて動かせば終わり、というイメージを持たれることもあります。ですが、実際にはそう単純ではありませんでした。最終的には、後編で扱う「ソースコードに存在しない列名のエラー」への対応まで必要になりました。
前編で扱うのは、そのエラーに出会う前の「準備」の段階です。地味な工程ですが、後の原因調査を大きく左右する重要な部分でした。
目次
この記事は以下の方を対象としています。
★5 Django開発経験が3年以上。
★4 Django開発経験が1年以上
★3 Webサイト開発経験あり。これからDjangoを学習します。
★2 Python 初級者。簡単なプログラムコードが書けます。
★1 プログラミング未経験。
まとめて最新化しない、という判断
バージョンアップでは、関連パッケージもまとめて最新にする進め方がよく採られます。一度に済ませたほうが、手間は減ります。ただ、これには落とし穴があります。
あとから不具合が出たとき、Django 5.2が原因なのか、別のパッケージが原因なのか、切り分けが難しくなるのです。
そこで今回は、着手前に方針を固めました。リリースノートに掲載された変更点をすべて洗い出し、自システムでの使用有無と、使用している場合の対応をまとめました。そのうえで、変更点を一覧化し、計画を立ててから作業に入りました。
この計画は有効でしたが、この後で実際に苦労した2つのトラブルは、いずれもリリースノートには記載のない挙動変化でした。計画的に備えても読み切れない部分があります。だからこそ、次に述べる切り分けの土台が効いてきます。
変更前の実行環境を基準点として確定する
最初に行ったのは、設定ファイルに書かれたバージョンではなく、実際にコンテナ内で動作している環境の確認です。確認結果は次の通りでした。
# 変更前の基準点(実コンテナ内で確認)
Python 3.12.2
Django 4.2.26
PostgreSQL 16.2
ここで重要だったのは、バージョン番号を確認すること自体ではありません。アップグレード後に問題が起きたとき、「何を変えたことで動作が変わったのか」を比較できる基準点を作ることでした。
この基準点は、後編の調査で実際に効いてきます。同じソースコード、同じPython、同じPostgreSQL、同じデータベース、同じ検索条件のまま、Djangoのバージョンだけを変えて比較する。この切り分けができたのは、変更前の環境を確定していたからです。もし全部を同時に更新していたら、問題が起きても、どの変更が原因なのか判断できなくなっていたはずです。
依存関係はrequirements.inを起点に、範囲を絞って更新する
このシステムでは、直接利用するパッケージを requirements.in に記載し、pip-tools の pip-compile で requirements.txt を生成しています。requirements.in が直接依存の定義、requirements.txt がそこから生成されるロックファイル、という位置付けです。
ここで大事なのは、requirements.txt を直接編集しないことです。requirements.txt はpip-compileが依存関係を解決して生成したファイルです。あるパッケージのバージョンだけを手で書き換えると、紐づく依存パッケージとの整合が取れなくなります。紐づくバージョンまで含めて適切に更新するには、requirements.in を変更元にして、pip-compileで生成し直す必要があります。
そのうえで、更新対象はできるだけ絞りました。requirements.txt を再生成すると、Djangoと無関係なパッケージにも差分が出ます。すべてを最新化すると、不具合発生時の原因候補が増えてしまいます。そこで、必要なパッケージだけを更新し、無関係な差分を増やさない方針にしました。これは、後で原因を切り分けるための準備でもあります。
最初のORMエラーは型の明示で解消した
Django 5.2へ変更した後、最初に問題になったのが Concat を使用した処理です。あるデータの名称や住所に、文字列とIDを連結する処理でした。連結対象のフィールドは CharField、IDは BigAutoField です。文字列型と数値型を一つの式で組み合わせており、Django 4.2では動作していましたが、5.2では型を明示する必要が生じました。
# 修正前:Django 4.2 では動作していたが、5.2 でエラーになった
Concat(
F("location_name"),
Value("deleted"),
F("id"), # ← BigAutoField をそのまま連結
)
# 修正後:型を明示して解消した
Concat(
F("location_name"),
Value("deleted"),
Cast(F("id"), CharField()), # ← 文字列へ変換
output_field=CharField(), # ← 連結結果全体の型も明示
)
対応として、Cast(F("id"), CharField()) でIDを文字列へ変換し、さらに output_field=CharField() を指定して連結結果全体の出力型も明示しました。この修正で処理は正常に動作しました。同じPythonコードでも、Djangoのバージョンが変わるとORMの式の型推論の挙動が変わることがあります。
この型の問題は、リリースノートには明記されていませんでした。動かして初めて表面化した挙動変化です。計画に含めていなかった変化だからこそ、先に現在地を整理していたことが切り分けに役立ちました。
準備は、速さより「あとで説明できること」のためにある
前編で伝えたいのは、修正の速さよりも、比較できる環境と、説明できる差分を先に作ることが重要である、という点です。現在地と変更範囲を整理しておいたことで、後から発生した問題を、Djangoの変更による影響として切り分けやすくなりました。急いで直したくなる場面ほど、この準備が効いてきます。
後編では、ソースコードに存在しない列名のエラーを、生成SQLとバージョン差分から追った過程をお伝えします。
※本記事のサムネイル画像は、生成AIを使用して作成しています。


