Django 2.2 LTS 主な変更点まとめ

Ryuji Tsutsui
CreditEngine Tech
Published in
13 min readApr 4, 2019
2系初のLTS版リリースおめでとう!

こんにちは、筒井です。2019年4月1日にDjango 2.2 LTSがリリースされました。今回は、以下公式サイトで紹介している主な変更点3つについて解説します。

Django 2.2 released | Weblog | Django

その他の変更点については、以下リリースノートを参照してください。

Django 2.2 release notes | Django documentation | Django

1. HttpRequest.headersでリクエストヘッダーの扱いが簡単に

リクエストヘッダーを扱うHttpRequest.headersが追加されました。
今まで使われてきたHttpRequest.METAと違って、キーが大文字・小文字を区別しません。
例えば、ユーザーエージェントを取得する場合は、以下の書き方のどちらでも同じ意味になります。

  • request.headers['User-Agent']
  • request.headers['user-agent']

HttpRequest.headersの使い方は dictと同じように見えますが、中身はdictではありません。django.utils.datastructures.CaseInsentiveMappingを継承してdict風に使えるようにしたdjango.http.request.HttpHeadersです。(HttpRequest.METAの場合はdictです)
django.utils.datastructures.CaseInsentiveMappingではキーをstr.lowerで変換して常に小文字にしているので、大文字・小文字の区別がなくなっています。

2. モデルでデータベースレベルの制約を定義できるように

モデルのMeta.constraintsでデータベースレベルの制約を定義できるようになりました。
今までもバリデーターで制約をかけることはできましたが、データベースに書き込む前にDjango側でチェックする仕組みなので、Django以外からデータを編集するシステムには効果がありませんでした。
そんな場合もMeta.constraintsを使えばデータを壊す心配がなくなります。
Meta.constraintsに渡せるクラスとして、以下の組み込みクラスが用意されています。

上記の使用例として以下のモデルを用意しました。

上記のモデルをshellコマンド上で実際に使ってみましょう。

# 18歳未満は登録できない
>>> Member.objects.create(name='テスト1', age=19) # 登録できる
<Member: Member object (1)>
>>> Member.objects.create(name='テスト2', age=18) # 登録できる
<Member: Member object (2)>
>>> Member.objects.create(name='テスト3', age=17) # 登録できない
Traceback (most recent call last):
(中略)
django.db.utils.IntegrityError: CHECK constraint failed: age_gte_18
# 同じ日に部屋の予約を重複させない
>>> from datetime import datetime
>>> today = datetime.today().date()
>>> Reservation.objects.create(room='テスト1', date=today) # 登録できる
>>> <Reservation: Reservation object (1)>
>>> Reservation.objects.create(room='テスト2', date=today) # 登録できる
<Reservation: Reservation object (2)>
>>> Reservation.objects.create(room='テスト1', date=today) # 登録できない
Traceback (most recent call last):
(中略)
IntegrityError: UNIQUE constraint failed: reservations_reservation.room, reservations_reservation.date
# 同一ユーザーはステータス'DRAFT'のデータを1個しか作れない
>>> from django.contrib.auth.models import User
>>> user1 = User.objects.create_user(username='user1')
>>> user2 = User.objects.create_user(username='user2')
>>> Post.objects.create(user=user1, status='DRAFT') # 登録できる<Post: Post object (1)>
>>> Post.objects.create(user=user2, status='DRAFT') # 登録できる
<Post: Post object (2)>
>>> Post.objects.create(user=user1, status='PUBLISHED') # 登録できる
<Post: Post object (3)>
>>> Post.objects.create(user=user1, status='DRAFT') # 登録できないTraceback (most recent call last):
(中略)
IntegrityError: UNIQUE constraint failed: posts_post.user_id

データベース上ではどんなテーブル定義になっているのか、 sqlmigrate コマンドで確認してみましょう。(データベースはSQLiteを使用)

# Memberモデルのテーブル定義
BEGIN;
--
-- Create model Member
--
CREATE TABLE "members_member" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(100) NOT NULL, "age" integer NOT NULL);
--
-- Create constraint age_gte_18 on model member
--
CREATE TABLE "new__members_member" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(100) NOT NULL, "age" integer NOT NULL, CONSTRAINT "age_gte_18" CHECK ("age" >= 18));
INSERT INTO "new__members_member" ("id", "name", "age") SELECT "id", "name", "age" FROM "members_member";
DROP TABLE "members_member";
ALTER TABLE "new__members_member" RENAME TO "members_member";
COMMIT;
# Reservationモデルのテーブル定義
BEGIN;
--
-- Create model Reservation
--
CREATE TABLE "reservations_reservation" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "room" varchar(20) NOT NULL, "date" date NOT NULL);
--
-- Create constraint unique_booking on model reservation
--
CREATE TABLE "new__reservations_reservation" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "room" varchar(20) NOT NULL, "date" date NOT NULL, CONSTRAINT "unique_booking" UNIQUE ("room", "date"));
INSERT INTO "new__reservations_reservation" ("id", "room", "date") SELECT "id", "room", "date" FROM "reservations_reservation";
DROP TABLE "reservations_reservation";
ALTER TABLE "new__reservations_reservation" RENAME TO "reservations_reservation";
COMMIT;
# Postモデルのテーブル定義
BEGIN;
--
-- Create model Post
--
CREATE TABLE "posts_post" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "status" varchar(20) NOT NULL, "user_id" integer NOT NULL REFERENCES "auth_user" ("id") DEFERRABLE INITIALLY DEFERRED);
--
-- Create constraint each_user_only_has_one_draft on model post
--
CREATE UNIQUE INDEX "each_user_only_has_one_draft" ON "posts_post" ("user_id") WHERE "status" = 'DRAFT';
CREATE INDEX "posts_post_user_id_a4f40dc7" ON "posts_post" ("user_id");
COMMIT;

以下の定義に注目してください。入力値に制約が設けられています。

# Memberモデルのテーブル定義
CONSTRAINT "age_gte_18" CHECK ("age" >= 18)
# Reservationモデルのテーブル定義
CONSTRAINT "unique_booking" UNIQUE ("room", "date")
# Postモデルのテーブル定義
CREATE UNIQUE INDEX "each_user_only_has_one_draft" ON "posts_post" ("user_id") WHERE "status" = 'DRAFT';

なお、この機能はバリデーターの代わりにはなりません。
上記モデルを使ってバリデーターなしの登録画面を作ると、制約に違反するデータはIntegrityError が発生します。(以下画面スクリーンショット参照)

バリデーターなしだと例外が発生

3. runserverのパフォーマンス改善

runserver実行中に大量のファイル変更があった場合のパフォーマンスを改善する機能が追加されました。
デフォルトではこの機能は無効になっています。以下がインストールされている環境で有効になります。

  • Watchman(ファイル監視ツール)
  • pywatchman(PythonからWatchmanを呼ぶライブラリ)

(参考URL:django-admin and manage.py | Django documentation | Django

実際に使ってみると、環境によってrunserver直後のメッセージが違っているのが分かります。

# デフォルト
$ python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
April 03, 2019 - 08:48:14
Django version 2.2, using settings 'django22.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
# Watchman + pywatchmanがインストールされている場合
$ python manage.py runserver
Watching for file changes with WatchmanReloader
Performing system checks...
System check identified no issues (0 silenced).
April 03, 2019 - 08:47:08
Django version 2.2, using settings 'django22.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

実装上はデフォルトではStatReloader、Watchman + pywatchmanがインストールされている場合はWatchmanReloaderでファイル監視を行うようになっています。
StatReloaderでは1秒間隔のポーリング、WatchmanReloaderはカーネルのシグナル経由でファイル変更を検知します。

なお、Watchman + pywatchmanを使う場合、Python以外のファイルを大量に含むディレクトリがあるとパフォーマンス上の問題を起こす場合があります。
例えば、npmやyarnを使っている場合はnode_modules/ がこれに該当します。
Watchmanには特定のディレクトリを監視対象外にする機能があります。以下ドキュメントを参考にWatchmanの設定ファイルを作成しておくと、この問題を回避できます。

Configuration Files | Watchman

--

--

Ryuji Tsutsui
CreditEngine Tech

私がmediumに記事を書くことはおそらく当分ないので、 https://ryu22e.org/ の方を読んでください。