Qiita Advent Calendar 2019 Django 15日目は、2019年12月2日にリリースされたDjango 3.0の主な変更点ついて紹介します。
祝・Django 3.0リリース!
公式サイトでのリリース情報は以下を参照してください。
Django 3.0 release notes | Django documentation | Django
MariaDB support# MariaDB を公式にサポートするようになりました。MariaDB 10.1以上に対応しています。
MariaDBを使う場合はMySQL用データベースバックエンドを使うようにしてください。
ASGI support# ついにDjangoでASGIをサポートするようになりました! これでDjangoでチャットのような非同期アプリケーションを気軽に作れるはずです。
それでは、早速サンプルアプリを作ってみましょう!
……と言いたいところですが、結局挫折してしまいました。公式ドキュメントAsynchronous support の以下説明を読んだところだと、3.0の時点ではASGIで通信できるようになっているものの、非同期のviewやmiddlewareの作成はまだサポートしていないようです。
Django has developing support for asynchronous (“async”) Python, but does not yet support asynchronous views or middleware; they will be coming in a future release.
There is limited support for other parts of the async ecosystem; namely, Django can natively talk ASGI, and some async safety support.
また、非同期イベントループの中で「非同期では安全ではない」コードを実行することができません(例: ORMの操作)。実行すると、SynchronousOnlyOperation
エラーが発生します。
SynchronousOnlyOperation
エラーを防ぐには、該当するコードの実行をasgiref.sync.async_to_sync
、またはスレッドプールを経由するように変更する必要があります。
Exclusion constraints on PostgreSQL# PostgreSQLの排他制約を作成するクラスExclusionConstraint
が追加されました。
PostgreSQLの排他制約についての解説記事は以下がオススメです。
PostgreSQLで排他制約がめっちゃ便利!! - そーだいなるらくがき帳
では、実際に使ってみましょう。データベースはPostgreSQL 9.6.16を使います。モデル定義は公式ドキュメントのサンプル をそのまま使います。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from django.contrib.postgres.constraints import ExclusionConstraint
from django.contrib.postgres.fields import DateTimeRangeField, RangeOperators
from django.db import models
from django.db.models import Q
class Room (models. Model):
number = models. IntegerField()
class Reservation (models. Model):
room = models. ForeignKey('Room' , on_delete= models. CASCADE)
timespan = DateTimeRangeField()
cancelled = models. BooleanField(default= False )
class Meta :
constraints = [
ExclusionConstraint(
name= 'exclude_overlapping_reservations' ,
expressions= [
('timespan' , RangeOperators. OVERLAPS),
('room' , RangeOperators. EQUAL),
],
condition= Q(cancelled= False ),
),
]
同じRoom
に対して時間帯が重なるReservation
が作れないように排他制約が指定されています。
なお、migrate
コマンド実行前に、psql
コマンド経由でCREATE EXTENSION btree_gist;
を実行してbtree_gist拡張 を有効にしておく必要があります。
migrate
コマンド実行後のテーブル定義を見てみましょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
Table "public.reservations_reservation"
Column | Type | Modifiers
-----------+-----------+-----------------------------------------------------------------------
id | integer | not null default nextval('reservations_reservation_id_seq' ::regclass)
timespan | tstzrange | not null
cancelled | boolean | not null
room_id | integer | not null
Indexes:
"reservations_reservation_pkey" PRIMARY KEY , btree (id)
"exclude_overlapping_reservations" EXCLUDE USING gist (timespan WITH && , room_id WITH = ) WHERE (cancelled = false )
"reservations_reservation_room_id_f7d9ba76" btree (room_id)
Foreign - key constraints :
"reservations_reserva_room_id_f7d9ba76_fk_reservati" FOREIGN KEY (room_id) REFERENCES reservations_room(id) DEFERRABLE INITIALLY DEFERRED
shell
コマンド経由で排他制約に違反するデータを作ってみましょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
>>> room = Room. objects. create(number= 1 )
>>> from datetime import datetime
>>> Reservation. objects. create(
... room= room,
... timespan= [datetime(2019 , 12 , 1 , 15 , 0 , 0 ), datetime(2019 , 12 , 2 , 15 , 0 , 0 )]
... )
< Reservation: Reservation object (1 )>
>>> # timespanが既存のReservationのtimespanと被っているのでエラーになる。
>>> Reservation. objects. create(
... room= room,
... timespan= [datetime(2019 , 12 , 2 , 14 , 0 , 0 ), datetime(2019 , 12 , 3 , 15 , 0 , 0 )]
... )
Traceback (most recent call last):
(中略)
django. db. utils. IntegrityError: conflicting key value violates exclusion constraint "exclude_overlapping_reservations"
(中略)
DETAIL: Key (timespan, room_id)= (["2019-12-02 14:00:00+00" ,"2019-12-03 15:00:00+00" ), 1 ) conflicts with existing key (timespan, room_id)= (["2019-12-01 15:00:00+00" ,"2019-12-02 15:00:00+00" ), 1 ).
>>> # timespanが被っていなければエラーにならない。
>>> Reservation. objects. create(
... room= room,
... timespan= [datetime(2019 , 12 , 2 , 16 , 0 , 0 ), datetime(2019 , 12 , 3 , 15 , 0 , 0 )]
... )
< Reservation: Reservation object (3 )>
公式ドキュメントのもう一つのサンプルも作ってみましょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from django.contrib.postgres.constraints import ExclusionConstraint
from django.contrib.postgres.fields import (
DateTimeRangeField,
RangeBoundary,
RangeOperators,
)
from django.db import models
from django.db.models import Func, Q
class TsTzRange (Func):
function = 'TSTZRANGE'
output_field = DateTimeRangeField()
class Reservation (models. Model):
# Roomの定義は先述のサンプルと同じ
room = models. ForeignKey('Room' , on_delete= models. CASCADE)
start = models. DateTimeField()
end = models. DateTimeField()
cancelled = models. BooleanField(default= False )
class Meta :
constraints = [
ExclusionConstraint(
name= 'exclude_overlapping_reservations' ,
expressions= (
(TsTzRange('start' , 'end' , RangeBoundary()), RangeOperators. OVERLAPS),
('room' , RangeOperators. EQUAL),
),
condition= Q(cancelled= False ),
),
]
先述のサンプルではtimespan
という1フィールドで予約時間帯を定義していましたが、これはstart
・end
に分けて、RangeBoundary クラスを使って排他制約を加えています。
migrate
コマンド実行後のテーブル定義を見てみましょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Table "public.reservations_reservation"
Column | Type | Modifiers
-----------+--------------------------+-----------------------------------------------------------------------
id | integer | not null default nextval('reservations_reservation_id_seq' ::regclass)
start | timestamp with time zone | not null
end | timestamp with time zone | not null
cancelled | boolean | not null
room_id | integer | not null
Indexes:
"reservations_reservation_pkey" PRIMARY KEY , btree (id)
"exclude_overlapping_reservations" EXCLUDE USING gist (tstzrange(start , "end" , '[)' ::text) WITH && , room_id WITH = ) WHERE (cancelled = false )
"reservations_reservation_room_id_f7d9ba76" btree (room_id)
Foreign - key constraints :
"reservations_reserva_room_id_f7d9ba76_fk_reservati" FOREIGN KEY (room_id) REFERENCES reservations_room(id) DEFERRABLE INITIALLY DEFERRED
Filter expressions# BooleanField
を出力する式はannotate
メソッドを経由しなくても直接filter
メソッドに渡せるようになりました。この説明だけだとピンと来ないかもしれないので、実際にコードを書いて検証してみましょう。
公式ドキュメントのサンプル を参考にして、以下のモデルを作ってみました。
1
2
3
4
5
6
7
8
9
10
11
12
from django.db import models
class Client (models. Model):
ACCOUNT_TYPES = (
('guest' , "ゲスト" ),
('staff' , "スタッフ" ),
)
account_type = models. CharField(
max_length= 5 ,
choices= ACCOUNT_TYPES,
)
まず、Django2.2のshell
コマンド上で上記モデルを使ってみましょう。
1
2
3
4
5
6
7
8
>>> from django.db.models import Exists, OuterRef
>>> non_unique_account_type = Client. objects. filter(
... account_type= OuterRef('account_type' ),
... ). exclude(pk= OuterRef('pk' )). values('pk' )
>>> Client. objects. filter(~ Exists(non_unique_account_type))
Traceback (most recent call last):
(中略)
TypeError : cannot unpack non- iterable Exists object
filter
メソッドにBooleanField
を出力する式(non_unique_account_type
)を渡すとTypeError
になりました。これを回避するには、annotate
メソッドで一旦フィールドを作ってからfilter
で参照する必要があります。
1
2
3
>>> Client. objects. annotate(
... non_unique_account_type=~ Exists(non_unique_account_type)
... ). filter(non_unique_account_type= True )
Django3.0では、この回避策を取らなくてもよくなりました。
1
2
3
4
5
6
>>> from django.db.models import Exists, OuterRef
>>> non_unique_account_type = Client. objects. filter(
... account_type= OuterRef('account_type' ),
... ). exclude(pk= OuterRef('pk' )). values('pk' )
>>> Client. objects. filter(~ Exists(non_unique_account_type))
< QuerySet [< Client: Client object (1 )> ]>
データベースには以下のクエリが発行されます。(django-debug-toolbar ==2.1 + SQLite 3.30.1で確認 )
1
2
3
4
5
6
7
8
9
10
>>> Client. objects. filter(~ Exists(non_unique_account_type))
SELECT "examples_client" . "id" ,
"examples_client" . "account_type"
FROM "examples_client"
WHERE NOT EXISTS
(SELECT U0. "id"
FROM "examples_client" U0
WHERE (U0. "account_type" = "examples_client" . "account_type"
AND NOT (U0. "id" = "examples_client" . "id" )))
LIMIT 21 [2.51 ms]
Enumerations for model field choices# Field.choices
にカスタム列挙型が使えるようになりました。
まず、2.2ではどのようなコードを書いていたのか見てみましょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from django.db import models
class Post (models. Model):
STATUS_PUBLISHED = 'published'
STATUS_DRAFT = 'draft'
STATUSES = (
(STATUS_PUBLISHED, "公開" ),
(STATUS_DRAFT, "下書き" ),
)
status = models. CharField(
max_length= 20 ,
choices= STATUSES,
default= STATUS_PUBLISHED,
)
choices
にはタプルで「データベースに保存する値」・「ユーザーに見せる文言」が指定されています。Post.status
を読み書きするコードの中で'draft'
・'publish'
を直に書かなくて済むよう、さらに定数を定義するようにしています。
主観的な問題なので違う意見の人もいるかもしれませんが、定数の中で別の定数を参照する、という設計は少々ややこしいと感じる人もいるのではないでしょうか。
3.0からは以下のように書けます。
1
2
3
4
5
6
7
8
9
10
11
12
13
from django.db import models
class Post (models. Model):
class Status (models. TextChoices):
PUBLISHED = 'published' , "公開"
DRAFT = 'draft' , "下書き"
status = models. CharField(
max_length= 20 ,
choices= Status. choices,
default= Status. DRAFT,
)
choices
の中身はStatus
クラスの定義だけを見れば分かるようになりました。
また、values
・labels
を使えば、定義内容の一覧を取得することもできます。
1
2
3
4
>>> Post. Status. values
['published' , 'draft' ]
>>> Post. Status. labels
['公開' , '下書き' ]
django-model-utils のChoice と似たような機能を公式でもサポートした感じですね。