Open9

Django周りのあれこれ

ピン留めされたアイテム
Hideki IkemotoHideki Ikemoto

このスクラップについて

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

Hideki IkemotoHideki Ikemoto

prefetch_relatedを使ってもクエリの数が減らない

prefetch_relatedの注釈より、

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

Hideki IkemotoHideki Ikemoto

モデルフィールドのvalidatorsを定義していても、フォームにバリデーションが必要

モデルフィールドには、デフォルトのフォームフィールドが用意されている
例えばPositiveIntegerFieldの場合、min_value=0となるフォームが用意される。
よって、-1を入れた場合はフォームレベルでバリデーションエラーとなる。

しかし、validatorsを入れても、それはフォームには反映されない。
最終的にsave()する際にValidationErrorになるのは同じだが、以下のようにエラ〜メッセージが不適切。
よって、フォーム側でも対処が必要。

  • num = PositiveSmallIntegerField(verbose_name="個数", default=1, validators=[MinValueValidator(1)]) と定義
  • 0を指定したときは画面に「この値は 1 以上でなければなりません。」と出る。
  • -1を指定したときは画面に「この値は 0 以上でなければなりません。」と出る(おかしい)。
Hideki IkemotoHideki Ikemoto

ForeignKeyがnull許容のときは、 on_delete=models.SET_NULL が使えないか考える。

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

Hideki IkemotoHideki Ikemoto

例外でHTTP 400を出したい

Http404 が例外として用意されているなら BadRequest(HTTP 400) を表す例外もあるかなと思ったんですが、ちょうどいいのはありません。

Django 3.1なら次の2つがありますが、2つとも問題があります。
MultiPartParserError は意味が違ってしまうし、 SuspiciousOperation はエラーログが出てしまいます

  • MultiPartParserError
  • SuspiciousOperation

Django 3.2には入りそうです。

Hideki IkemotoHideki Ikemoto

マイグレーションファイルがおかしくなるパターン

1. 既存のマイグレーションファイルを上書き

開発中や、マージされる前のPull Requestに対してマイグレーションファイルを作り直すのは良いですが、すでにリリースされたものを上書きするのはやめましょう。

ただし例外はあります。

2. フィールドのdefaultがcallableで、その実装が変わった場合

具体的には、過去に「ランダムで発行されてた文字列」で実装されていたものを、「ランダムで発行しつつ新規に作ったテーブルに保存」に変えた場合です。この場合、途中からのマイグレーションは出来ますが、ゼロからマイグレーションしようとすると、エラーになります。

これを解消するためには、過去のマイグレーションファイルのdefaultを「ランダムで発行されてた文字列」となる、別のcallableに変える必要があります。そもそも、callableを変えずに、別のcallableをdefaultに変えれば問題なかったんですが。。。

ちなみに、「ランダムで発行しつつ新規に作ったテーブルに保存」にしたら、defaultが複数回呼び出されるため、無駄にレコードが出来ることが分かりました。まあダメって程ではないですが、defaultをやめるべきでしたね。。。

Hideki IkemotoHideki Ikemoto

時刻の扱い(記載中)

結論(暫定?)

  • 設定
    * USE_TZ=True
    * TIME_ZONE="Asia/Tokyo"
    * DBはUTC
  • django.utils.timezone を使う。
  • 時刻を取得する際は timezone.now() を使う。
  • アプリケーションコードでローカル時刻が必要なとき: これの2.
  • 日付をローカル時刻でパースしたい場合: これの1.

詳細

まず、Pythonのdatetimeオブジェクトには、タイムゾーンを含むawareオブジェクトと、タイムゾーンを含まない、naiveオブジェクトがあります。
https://docs.python.org/ja/3/library/datetime.html

で、Djangoのタイムゾーンの扱いはこれ。以下はUSE_TZ=Trueかつ、TIME_ZONE="Asia/Tokyo"と仮定します。
https://docs.djangoproject.com/ja/3.1/topics/i18n/timezones/

  • DBにはUTCで保存
  • テンプレートやフォームでタイムゾーンに合わせて変換

が基本。

Hideki IkemotoHideki Ikemoto

get_absolute_url()

モデルにget_absolute_urlの実装を入れると、success_urlがなくても勝手にインスタンスのget_absolute_urlを呼んでくれる。

https://github.com/django/django/blob/76c0b32f826469320c59709d31e2f2126dd7c505/django/views/generic/edit.py#L110-L121

どのモデルかを判定しなくて済むので、テンプレートにごちゃごちゃ書いたりしなくて済む。

自分で開発しているものでは同様に、次のようなメソッドを定義している。CRUDLで済むページにはあると良さげ。

  • get_edit_url()
  • get_delete_url()
  • get_add_url() (staticmethod)
  • get_list_url() (staticmethod)