Django周りのあれこれ

このスクラップについて
プライベートでDjangoアプリを作っていますが、そこで得たノウハウをまとめるものです。

prefetch_relatedを使ってもクエリの数が減らない
prefetch_relatedの注釈より、
いつもの QuerySet と同じく、異なるデータベースクエリを意味するチェーンメソッドは、読み込み済みのキャッシュを無視して新たなデータベースクエリを発行します。

モデルフィールドのvalidatorsを定義していても、フォームにバリデーションが必要
モデルフィールドには、デフォルトのフォームフィールドが用意されている
例えばPositiveIntegerFieldの場合、min_value=0となるフォームが用意される。
よって、-1を入れた場合はフォームレベルでバリデーションエラーとなる。
しかし、validatorsを入れても、それはフォームには反映されない。
最終的にsave()する際にValidationErrorになるのは同じだが、以下のようにエラ〜メッセージが不適切。
よって、フォーム側でも対処が必要。
-
num = PositiveSmallIntegerField(verbose_name="個数", default=1, validators=[MinValueValidator(1)])
と定義 - 0を指定したときは画面に「この値は 1 以上でなければなりません。」と出る。
- -1を指定したときは画面に「この値は 0 以上でなければなりません。」と出る(おかしい)。

on_delete=models.SET_NULL
が使えないか考える。
ForeignKeyがnull許容のときは、 on_delete=models.PROTECT
の場合、ForeignKey 側のモデルを削除しようとすると ProtectedError になる。これを避けるための方法として、 SET_NULL
が使えないか考えると良さそう。

例外でHTTP 400を出したい
Http404 が例外として用意されているなら BadRequest(HTTP 400) を表す例外もあるかなと思ったんですが、ちょうどいいのはありません。
Django 3.1なら次の2つがありますが、2つとも問題があります。
MultiPartParserError は意味が違ってしまうし、 SuspiciousOperation はエラーログが出てしまいます。
- MultiPartParserError
- SuspiciousOperation
Django 3.2には入りそうです。

CharFieldのデフォルトは?
_get_defaultの実装によれば、空文字列のようです。便利と言えば便利ですが、トラブルの元になりそうな感じもします。
ちなみに interprets_empty_strings_as_nulls
は Oracle対策のようです。

マイグレーションファイルがおかしくなるパターン
1. 既存のマイグレーションファイルを上書き
開発中や、マージされる前のPull Requestに対してマイグレーションファイルを作り直すのは良いですが、すでにリリースされたものを上書きするのはやめましょう。
ただし例外はあります。
2. フィールドのdefaultがcallableで、その実装が変わった場合
具体的には、過去に「ランダムで発行されてた文字列」で実装されていたものを、「ランダムで発行しつつ新規に作ったテーブルに保存」に変えた場合です。この場合、途中からのマイグレーションは出来ますが、ゼロからマイグレーションしようとすると、エラーになります。
これを解消するためには、過去のマイグレーションファイルのdefaultを「ランダムで発行されてた文字列」となる、別のcallableに変える必要があります。そもそも、callableを変えずに、別のcallableをdefaultに変えれば問題なかったんですが。。。
ちなみに、「ランダムで発行しつつ新規に作ったテーブルに保存」にしたら、defaultが複数回呼び出されるため、無駄にレコードが出来ることが分かりました。まあダメって程ではないですが、defaultをやめるべきでしたね。。。

時刻の扱い(記載中)
結論(暫定?)
- 設定
* USE_TZ=True
* TIME_ZONE="Asia/Tokyo"
* DBはUTC - django.utils.timezone を使う。
- 時刻を取得する際は
timezone.now()
を使う。 - アプリケーションコードでローカル時刻が必要なとき: これの2.
- 日付をローカル時刻でパースしたい場合: これの1.
詳細
まず、Pythonのdatetimeオブジェクトには、タイムゾーンを含むawareオブジェクトと、タイムゾーンを含まない、naiveオブジェクトがあります。
で、Djangoのタイムゾーンの扱いはこれ。以下はUSE_TZ=Trueかつ、TIME_ZONE="Asia/Tokyo"と仮定します。
- DBにはUTCで保存
- テンプレートやフォームでタイムゾーンに合わせて変換
が基本。

get_absolute_url()
モデルにget_absolute_urlの実装を入れると、success_urlがなくても勝手にインスタンスのget_absolute_urlを呼んでくれる。
どのモデルかを判定しなくて済むので、テンプレートにごちゃごちゃ書いたりしなくて済む。
自分で開発しているものでは同様に、次のようなメソッドを定義している。CRUDLで済むページにはあると良さげ。
get_edit_url()
get_delete_url()
-
get_add_url()
(staticmethod) -
get_list_url()
(staticmethod)