Django 4.2から5.2へ(後編)コードに存在しない列名エラーの追跡

皆さま、こんにちは。グローバルウェイのKです。
前編では、バージョンアップ前の準備として、変更前の環境を基準点として確定し、依存関係の変更範囲を絞った話をお伝えしました。後編は、その準備が実際に効いてくる場面です。
扱うのは、ある検索処理で発生した500エラーです。データベースからは、ソースコードに存在しない列名が曖昧だというエラーが返ってきました。原因は、アプリケーションコード、Django ORM、生成SQL、PostgreSQLという複数の層にまたがっていました。以下では、その切り分けの過程を順に説明します。
目次
この記事は以下の方を対象としています。
★5 Django開発経験が3年以上。
★4 Django開発経験が1年以上
★3 WEBサイト開発経験あり。これからDjangoを学習します。
★2 Python 初級者。簡単なプログラムコードが書けます。
★1 プログラミング未経験。
コードに存在しない列名で500エラーが出た
Django 5.2に変更した後、運用担当者向けアプリケーションのある検索処理で500エラーが発生しました。問題は、クエリを組み立てた時点ではなく、評価した時点で起きました。生成SQLを表示するところまでは進みますが、結果を取り出すためにクエリを評価した段階で、データベースがエラーを返します。
内容は、ある列名の参照が曖昧であるというものでした。同じ名前の列が複数あり、どちらを指すか判断できないという意味です。ただし、その列名はソースコードに存在しませんでした。取得列の指定を確認しても見当たりません。この列名がどこから来たのかを、次に調べました。
長い別名がPostgreSQLの63バイト制限で切り詰められていた
取得列の指定には、テーブルを多段にたどる非常に長い関連参照が2つ含まれていました。実際の名称を一般化していますが、構造は次の通りです。末尾の name_code と person_type だけが異なり、先頭の共通部分が非常に長い、という形です。
# values() に指定していた、2つの長い関連参照
queryset.values(
"hoge_fuga_piyo_hogera_hogehoge__hoge_fuga_piyo_hogera__created_by__name_code",
"hoge_fuga_piyo_hogera_hogehoge__hoge_fuga_piyo_hogera__created_by__person_type",
)
PostgreSQLの識別子は、標準設定では最大63バイトです。これを大まかにいうと、名前が長すぎると途中で強制的に切り詰められる、ということです。この2つは76文字と78文字あり、末尾の異なる部分に到達する前に、どちらも同じ位置で切り詰められます。
# 本来は末尾が異なる、別々の2つの関連参照
..._hoge_fuga_piyo_hogera__created_by__name_code
..._hoge_fuga_piyo_hogera__created_by__person_type
↓ PostgreSQL が 63 バイトで切り詰める
# どちらも同じ別名になり、区別できなくなる
hoge_fuga_piyo_hogera_hogehoge__hoge_fuga_piyo_hogera__created_
hoge_fuga_piyo_hogera_hogehoge__hoge_fuga_piyo_hogera__created_
結果として、データベースからは同じ名前の列が2つ並んで見え、区別できません。これが、曖昧だというエラーの原因でした。表示されたコードにない列名は、Djangoが生成した長い別名を、PostgreSQLが切り詰めた後の姿です。
63バイト制限だけでは4.2との差を説明できなかった
切り詰めが起きること自体は、これで分かりました。ただし、この説明には残る点がありました。63バイト制限はDjangoのアップグレードで加わったものではなく、データベースもアプリケーションコードも同じです。それにもかかわらず、Django 4.2.26では正常、5.2ではエラー、という差がありました。制限だけが原因なら、4.2でも同じエラーが起きるはずです。 この差は、Djangoのバージョンによって生成SQLが異なることを示していました。なお、この挙動変化はリリースノートに明記されておらず、調査した範囲では、同じ現象の事例をWeb上で見つけられませんでした。そのため、同名の列の衝突を疑ってコードを確認し、該当がないことから識別子の切り詰めを疑って長さを数える、という手順で、仮説と検証を手元で往復させながら進めました。63バイト制限にたどり着いた後も4.2との差が残ったため、次は生成SQLの比較に進みました。
Djangoのバージョンだけを変えて比較した
原因を切り分けるため、条件をそろえて比較しました。前編で確定した基準点を使い、同じソースコード、同じデータベース、同じPython、同じ検索条件のまま、Djangoのバージョンだけを変えます。比較したのは、Django 4.2.26、5.2、5.2.11の3つです。画面操作では余計な条件が混ざるため、対象の処理をシェルから直接呼び出して再現しました。
# 他をすべて固定し、Django のバージョンだけを変えて比較
ソースコード 固定
データベース 固定
Python 固定
検索条件 固定
Django 4.2.26 → 5.2 → 5.2.11 # これだけ変える
生成SQLを比較すると、values() や values_list() で取得する列の別名の扱いに、バージョン間で違いがありました。Django 5.2では、指定した名前を保持する形で別名が明示的に付くケースがあります。ただし、すべてのSQL差分が今回のエラーに直結したわけではありません。差の有無と、それが今回の原因かどうかは分けて確認しました。
原因はORMの別名変化とデータベース制限の組み合わせだった
原因をより正確に理解するため、Django本体の実装も確認しました。values() で指定した名前が、どうSELECT列と別名に変換されるのか。Query.selected や SQLCompiler.get_select() を追い、取得列の別名がどこで生成されるかに的を絞って読み進めました。
# 3つが重なったときだけ、エラーが起きる
長い関連参照 ┐
Django 5.2 の別名変化 ┼→ 63バイトで別名が衝突 → ambiguous エラー
PostgreSQL 63バイト制限 ┘
最終的には、原因は、複数の層が交差した構造にあると分かりました。長い関連参照、Django 5.2での別名の扱いの変化、PostgreSQLの63バイト制限。今回の環境では、この3つが重なったときにエラーが発生しました。「Django 5.2が悪い」とも「PostgreSQLが悪い」とも言い切れません。Django 5.2で別名の扱いが変わり、以前は表面化していなかったPostgreSQLの制限との衝突が、初めて表に出た、ということです。
短い別名を明示して衝突を回避する
修正方針はシンプルです。長い関連参照そのものは変えず、F() を使って短く意味の分かる別名を明示します。関連やモデル、取得する内容は変えないため、影響範囲を最小限に抑えられます。
# 修正前:長い関連参照をそのまま values() に渡していた
# → 長い名前が別名になり、63バイトで切り詰められて衝突する
queryset.values(
"hoge_fuga_piyo_hogera_hogehoge__hoge_fuga_piyo_hogera__created_by__name_code",
"hoge_fuga_piyo_hogera_hogehoge__hoge_fuga_piyo_hogera__created_by__person_type",
)
# 修正後:F() で短い別名を明示する
from django.db.models import F
queryset.values(
created_by_name=F( # ← 短い別名を付ける
"hoge_fuga_piyo_hogera_hogehoge__hoge_fuga_piyo_hogera__created_by__name_code"
),
created_by_type=F(
"hoge_fuga_piyo_hogera_hogehoge__hoge_fuga_piyo_hogera__created_by__person_type"
),
)
-- 生成SQL:短い別名になり、切り詰めが起きない
SELECT
... AS "created_by_name",
... AS "created_by_type"
FROM ...
修正前は、長い関連参照を values() にそのまま渡していました。この場合、その長い名前がSELECT列の別名として使われ、63バイトで切り詰められます。修正後は、created_by_name と created_by_type という短い別名を F() で与えます。生成SQLでも短い別名が使われ、切り詰めによる衝突は起こりません。モデル間の関連や取得内容を変えずに、識別子長制限を回避できました。
まとめ
後編でお伝えしたかったのは、次の点です。
アプリケーションコードが同じでも、Djangoのバージョンが変われば、データベースに届くSQLまで同じとは限りません。
63バイト制限を見つけた時点で調査を終えていたら、4.2で動いていた理由を説明できないままでした。説明できない差が残る限り、次の仮説へ進んだことで、複数の層が交差して生じた原因にたどり着けました。原因が分からないときほど、生成SQLを確認する価値があります。同様の障害に向き合う方の参考になれば幸いです。
※本記事のサムネイル画像は、生成AIを使用して作成しています。

