2022年8月3日、Django 4.1がリリースされました。

4.1リリースを祝してジャンゴ・ラインハルトと一緒にパーティ!(Midjourneyで作成)

4.1リリースを祝してジャンゴ・ラインハルトと一緒にパーティ!(Midjourneyで作成)

公式サイトでのリリース情報は以下を参照してください。

Django 4.1 release notes | Django documentation | Django

4.1のサポート期限は2023年12月です。3.2 LTSからアップデートするとサポート期限が短くなってしまうことに注意してください(3.2 LTSのサポート期限は2024年4月)。 サポート期限を短くしたくない場合は、2023年4月リリース予定の4.2 LTSまでアップデートしない手もあります(4.2 LTSのサポート期限は2026年4月)。 各バージョンのサポート期限についての詳細は以下公式ドキュメント「Supported Versions」を参照してください。

Download Django | Django

それでは、主な変更点について紹介します。

Asynchronous handlers for class-based views

ついに非同期クラスベースViewが書けるようになりました。Django 3.1では非同期関数ベースViewをサポートするようになりましたが、これで関数、クラス両方で非同期Viewを書けます。

非同期クラスベースViewの書き方は、メソッドの先頭にasyncを入れるだけです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import asyncio

from django.http import HttpResponse
from django.views import View


class AsyncView(View):
    # メソッドの先頭にasyncを付ける
    async def get(self, request, *args, **kwargs):
        await asyncio.sleep(1)
        return HttpResponse("非同期ビューのレスポンス")

Asynchronous ORM interface

Model操作が非同期インタフェースを提供するようになりました。

create()get()delete()などのメソッドの非同期用は、先頭にaを付けたメソッドacreate()aget()adelete()です。

また、all()filter()などを使ってfor文でデータを取り出したい場合は、forの先頭にasyncを付けます。

同期処理用と非同期処理用で書き方が2通りできて難しくなったように感じるかもしれませんが、あまり頑張って覚えようとしなくても大丈夫です。 基本的には従来どおりの同期処理用の書き方を覚えておけば良くて、非同期処理で使えない場合はDjangoがSynchronousOnlyOperationエラーを発生させて教えてくれます。 そうなった場合は、前述の「先頭にaを付けたメソッド」か「forの先頭にasync」のどちらかのパターンでエラーを回避できます。

以下で非同期処理でのModel操作の例を紹介します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from django.http import HttpResponse
from django.views import View

from .models import Book


class AsyncView(View):
    async def get(self, request, *args, **kwargs):
        # メソッドの先頭にaを付ける(awaitを使うのも忘れずに)
        book = await Book.objects.afirst()
        book = await Book.objects.aget(pk=book.pk)

        titles = []
        # forの前にasyncを付ける
        async for book in Book.objects.all():
            titles.append(book.title)
        return HttpResponse(",".join(titles))

ただし、Model操作は非同期インタフェースを提供しているものの、4.1時点では裏側のデータベース接続は同期処理のままです。非同期処理の中で実行してもデータの損失、破損が発生しない安全な処理にはなっていますが、ボトルネックになる可能性はあります。

非同期でのデータベース接続は将来のバージョンでサポートされる予定です。

Validation of Constraints

ModelのMeta.constraintsに指定したデータベースへの制約をバリデーションで使えるようになりました。

実際の動作を確認するため、以下のModelをCreateViewで登録する画面を作ってみましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from django.db import models


class Example(models.Model):
    age = models.IntegerField()

    class Meta:
        constraints = [
            models.CheckConstraint(
                check=models.Q(age__gte=18),
                name="age_gte_18",
            ),
        ]

Django 4.0の場合、ageに18未満を入力しようとすると、以下のようにIntegrityErrorが発生します。

IntegrityErrorが発生

IntegrityErrorが発生

Django 4.1では以下のようにエラーメッセージが表示されます。

`エラーメッセージが表示される

`エラーメッセージが表示される

violation_error_messageを指定すればエラーメッセージのカスタマイズもできます。

前述の例のエラーメッセージを変更する場合は以下のようにします。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from django.db import models


class Example(models.Model):
    age = models.IntegerField()

    class Meta:
        constraints = [
            models.CheckConstraint(
                check=models.Q(age__gte=18),
                name="age_gte_18",
                # ↓これを追加
                violation_error_message="18歳未満は登録しちゃ駄目",
            ),
        ]

Form rendering accessibility

{{ form }}でレンダリングされるフォームのテンプレートに、divタグベースの新しいものが追加されました。

スクリーンリーダーなどサイトの自動読み取りを行うツールでは、divタグを使ったサイトの方が読み取りやすいということで、追加されたようです。

ただし、divタグベースの出力はDjango 4.1時点で非デフォルトです。

divタグベースを使いたい場合は、FORM_RENDERERdjango.forms.renderers.DjangoDivFormRendererまたはdjango.forms.renderers.Jinja2DivFormRendererを指定します。

ただし、django.forms.renderers.DjangoDivFormRendererdjango.forms.renderers.Jinja2DivFormRendererはDjango 5.0で非推奨(Django 6.0で廃止予定)になるので注意してください。 Django 5.0からはdivタグベースの方がデフォルトになります。

では、実際の出力内容を見てみましょう。 まずは現在のデフォルトdjango.forms.renderers.DjangoTemplatesの出力内容です。(見やすくするためにスペースや改行を取り除いています)。

1
2
3
4
5
6
<tr>
  <th><label for="id_age">Age:</label></th>
  <td>
    <input type="number" name="age" required id="id_age">
  </td>
</tr>

これは従来と同じ内容です。次に、新しく追加されたテンプレートクラスdjango.forms.renderers.DjangoDivFormRendererの出力内容です。

1
2
3
4
<div>
  <label for="id_age">Age:</label>
  <input type="number" name="age" required id="id_age">
</div>

divタグで囲ったフォームが出力されました。

TrueのときCookieをマスキングできる設定CSRF_COOKIE_MASKEDが追加されました。

新機能ですが、いきなり非推奨扱いになっています。Django 5.0で削除される予定です。

DjangoではもともとBREACH攻撃対策としてCookieをマスキングしていたのですが、対策として意味がないと判明したため、Django 4.1からはデフォルトでマスキングなしになりました。CSRF_COOKIE_MASKEDは下位互換性のために追加された機能です。基本的には使わないようにしましょう。

CSRF_COOKIE_MASKEDが実装されるまでの議論の過程は以下を参照してください。

#32800 (CsrfViewMiddleware unnecessarily masks CSRF cookie) – Django

Log out via GET

ログアウト機能を提供する汎用View django.contrib.auth.views.LogoutViewへのGETリクエストが非推奨となり、POSTリクエストを受けるけるようになりました。

GETリクエストでのログアウトはDjango 5.0で廃止される予定です。

POSTリクエストのログアウトボタンをリンク風UIにするには、CSSを使う必要があります。以下リリースノートの例が参考になります。

https://docs.djangoproject.com/en/4.1/releases/4.1/#log-out-via-get

上記の例を実装すると、ブラウザ上では以下のように表示されます。

リンク風ログアウトボタン

リンク風ログアウトボタン

この変更を採用するに至った議論は以下のチケットに載っているので、興味がある方は読んでみてください(なんと、クローズするまで10年以上かかっています…!)。

#15619 (Logout link should be protected) – Django