Django 5.0 主な変更点まとめ

これは[Qiita Django Advent Calendar 2023](https://qiita.com/advent-calendar/2023/django) 11日目の記事です。

2023年12月4日、Django 5.0がリリースされました。主な変更点について解説します。

![Django5.0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7809/7545e7ef-82d1-e9db-d8fc-671e31f93759.png "Django 5.0リリースおめでとう！（Created with DALL-E）")
*Django 5.0リリースおめでとう！（Created with DALL-E）*

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

https://docs.djangoproject.com/en/dev/releases/5.0/

Django 5.0はlong-term support（LTS）版ではありません。サポート期限は2025年4月です。
LTS版である4.2のサポート期限（2026年4月）より短いため、バージョンアップするメリットがあるのか慎重に検討してください。
各バージョンのサポート期限についての詳細は以下公式ドキュメント「Supported Versions」を参照してください。

https://www.djangoproject.com/download/

## Facet filters in the admin
adminのフィルターに該当件数（ファセット数）を表示できるようになりました。[ModelAdmin.show_facets](https://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.ModelAdmin.show_facets)属性で設定を変更できます。以下が設定例です。

```python:admin.py
from django.contrib import admin

from .models import Book

class BookAdmin(admin.ModelAdmin):
    list_display = ("title", "price", "author",)
    list_filter = ("author",)
    show_facets = admin.ShowFacets.ALWAYS  # これを設定

admin.site.register(Book, BookAdmin)
```

実際のadminの画面は以下のように表示されます。

![Select_book_to_change___Django_site_admin.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7809/ed5686ee-ef04-30e6-d036-efddfef1d3aa.jpeg)

ファセット数を表示させると別途クエリが発行されるので、パフォーマンスに影響を及ぼす可能性があることに注意してください。

`ModelAdmin.show_facets`属性に設定できる値は以下の3種類です[^show_facets]。
[^show_facets]: 参考URL: https://docs.djangoproject.com/en/5.0/ref/contrib/admin/#django.contrib.admin.ModelAdmin.ShowFacets

- `admin.ShowFacets.ALWAYS`: 常にファセット数を表示
- `admin.ShowFacets.ALLOW`: ファセット数の表示・非表示を切り替えられる（後述）
- `admin.ShowFacets.NEVER`: ファセット数を表示させない

`admin.ShowFacets.ALLOW`は以下のリンクが表示されて、ファセット数の表示・非表示を切り替えられます。
![Select_book_to_change___Django_site_admin.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7809/b682e87f-211b-d39a-5b38-e708ba7be8ea.jpeg)

## Simplified templates for form field rendering
独自のフォームフィールドテンプレートを作成する際の書き方がよりシンプルになりました。
まず、Django 4.2での書き方を見てください。色々なメソッドを組み合わせる必要があるため、かなり複雑な書き方にする必要があります。

```django
<form>
...
<div>
  {{ form.name.label_tag }}
  {% if form.name.help_text %}
    <div class="helptext" id="{{ form.name.id_for_label }}_helptext">
      {{ form.name.help_text|safe }}
    </div>
  {% endif %}
  {{ form.name.errors }}
  {{ form.name }}
  <div class="row">
    <div class="col">
      {{ form.email.label_tag }}
      {% if form.email.help_text %}
        <div class="helptext" id="{{ form.email.id_for_label }}_helptext">
          {{ form.email.help_text|safe }}
        </div>
      {% endif %}
      {{ form.email.errors }}
      {{ form.email }}
    </div>
    <div class="col">
      {{ form.password.label_tag }}
      {% if form.password.help_text %}
        <div class="helptext" id="{{ form.password.id_for_label }}_helptext">
          {{ form.password.help_text|safe }}
        </div>
      {% endif %}
      {{ form.password.errors }}
      {{ form.password }}
    </div>
  </div>
</div>
...
</form>
```

Django 5.0では [as_field_group()](https://docs.djangoproject.com/en/dev/ref/forms/api/#django.forms.BoundField.as_field_group)メソッドを使ってようにシンプルに書けます。以下の書き方は前述のテンプレートと同じ意味です。

```django
<form>
...
<div>
  {{ form.name.as_field_group }}
  <div class="row">
    <div class="col">{{ form.email.as_field_group }}</div>
    <div class="col">{{ form.password.as_field_group }}</div>
  </div>
</div>
...
</form>
```

## Database-computed default values
デフォルト値を設定する[Field.db_default](https://docs.djangoproject.com/en/dev/ref/models/fields/#django.db.models.Field.db_default)属性が追加されました。従来の[Field.default](https://docs.djangoproject.com/en/dev/ref/models/fields/#django.db.models.Field.default)属性と違って、データベース側で計算した値を設定できます。

以下がコード例です。

```python
from django.db import models
from django.db.models.functions import Now, Pi

class Example(models.Model):
    name = models.CharField(max_length=100)
    created_at = models.DateTimeField(db_default=Now())
    circumference = models.FloatField(db_default=2 * Pi())
```

上記モデルを使ってデータを登録すると以下のような結果になります。

```python
>>> from example.models import Example
>>> example = Example.objects.create(name="example")
>>> example.created_at
datetime.datetime(2023, 11, 14, 14, 52, 50, 579000, tzinfo=datetime.timezone.utc)
>>> example.circumference
6.283185307179586
```

ただし、`Field.db_default`属性は以下のように他のフィールドを参照する値を指定できません。

```python
from django.db import models
from django.db.models import F
from django.db.models import Value as V
from django.db.models.functions import Concat

class Example(models.Model):
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    full_name = models.TextField(
        # NG
        db_default=Concat(F("first_name"), V(" "), F("last_name")),
    )
```

上記のモデル定義を書いた状態で`migrate`コマンドを実行すると、以下のエラーが発生します。

```shell
$ python manage.py migrate
SystemCheckError: System check identified some issues:

ERRORS:
example.Example.full_name: (fields.E012) Concat(ConcatPair(F(first_name), ConcatPair(Value(' '), F(last_name)))) cannot be used in db_default.
```

## Database generated model field
データベースで生成した値を扱う[GeneratedField](https://docs.djangoproject.com/en/dev/ref/models/fields/#django.db.models.GeneratedField)が追加されました。
以下の例では`first_name`と`last_name`を参照した値を生成する`full_name`というフィールドに`GeneratedField`を使っています。

```python
from django.db import models
from django.db.models import F
from django.db.models import Value as V
from django.db.models.functions import Concat

class Example(models.Model):
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    full_name = models.GeneratedField(
        expression=Concat(F("first_name"), V(" "), F("last_name")),
        db_persist=True,  # TrueかFalseを必ず指定する
    )
```

`db_persist`は`True`のとき値をストレージに保存する設定です。必ず`True`か`False`を指定する必要があります。どちらを指定できるかは使用するデータベースによって異なります。

上記のモデルを使ってデータを登録してみましょう。登録後に`full_name`が生成されているのがわかります。また、登録後に`first_name`、`last_name`を更新すると、`full_name`も更新されます。

```python
>>> from example.models import Example
>>> example = Example.objects.create(first_name="Taro", last_name="Yamada")
>>> example.full_name  # first_nameとlast_nameの登録内容が反映される
'Taro Yamada'
>>> Example.objects.filter(pk=example.pk).update(first_name="Hanako", last_name="Suzuki")
1
>>> example.refresh_from_db()
>>> example.full_name  # first_nameとlast_nameの変更内容が反映される
'Hanako Suzuki'
```

テーブル定義も見てみましょう（本記事ではSQLite 3.39.5を使っています）。

```shell
$ python manage.py sqlmigrate example 0001
BEGIN;
--
-- Create model Example
--
CREATE TABLE "example_example" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "first_name" varchar(100) NOT NULL, "last_name" varchar(100) NOT NULL, "full_name" varchar(100) GENERATED ALWAYS AS (COALESCE("first_name", '') || COALESCE(COALESCE(' ', '') || COALESCE("last_name", ''), '')) STORED);
COMMIT;
```

`full_name`の定義が`"full_name" varchar(100) GENERATED ALWAYS AS (COALESCE("first_name", '') || COALESCE(COALESCE(' ', '') || COALESCE("last_name", ''), '')) STORED)`になっています。データベースの機能を使って値を生成していることがわかります。

## More options for declaring field choices
[Field.choices](https://docs.djangoproject.com/en/dev/ref/models/fields/#django.db.models.Field.choices)属性、[ChoiceField.choices](https://docs.djangoproject.com/en/dev/ref/forms/fields/#django.forms.ChoiceField.choices)属性に指定できる値の種類が2つ増えました。

1つ目は、[列挙型](https://docs.djangoproject.com/en/dev/ref/models/fields/#field-choices-enum-types)を指定する際、列挙型の`.choices`属性を使わず列挙型を直接指定できるようになりました。

まず、Django 4.2までの列挙型の使い方を見てください。

```python
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,  # choices属性を指定する
        default=Status.DRAFT,
    )
```

Django 5.0からは以下のように書けます。

```python
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,
    )
```

2つ目は、以下のようにcallableなオブジェクトも渡せます。

```python
from django.db import models

def get_statuses():
    return {
        "published": "公開",
        "draft": "下書き",
    }

class Post(models.Model):
    status = models.CharField(
        max_length=20,
        choices=get_statuses,
        default="draft",
    )
```