Django 2.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の設定ファイルを作成しておくと、この問題を回避できます。