Python学習チャンネル by PyQ

Pythonのオンライン学習プラットフォームPyQのオフィシャルブログです

Djangoの最新バージョン、Django5.0を紹介します

こんにちは、PyQサポートです。
2023年12月4日に Django 5.0 がリリースされました。
この記事では、Django 5.0のリリースノート から、次の項目について紹介します。

Django 5.0の主な新機能(カッコ内は意訳です)

  • Facet filters in the admin (管理画面のフィルターに件数を表示)
  • Simplified templates for form field rendering (テンプレートでのフォームフィールドの記述を簡素化)
  • Database-computed default values (データベースレベルでのデフォルト制約)
  • Database generated model field (生成列を定義できるモデルフィールドの追加)
  • More options for declaring field choices (より書きやすくなった選択項目の記述方法)

Django 5.0 リリースの概要

名称Django
リリースバージョンDjango5.0
リリース日2023年12月4日
サポートしているPython3.10, 3.11, 3.12
サポートしているPythonhttps://docs.djangoproject.com/en/5.0/releases/5.0/

※アップデートの際はBackwards Incompatible Changes(後方互換性のない変更)に十分注意してください

Django 5.0 で追加された新機能

Facet filters in the admin

管理画面のフィルターに、絞り込みした件数が表示されるようになりました。

この件数の表示は、以下のようにshow_facetsオプションで「常に表示(ALWAYS)」「表示・非表示の切り替え可(ALLOW)」「常に非表示(NEVER)」のいずれかを設定できます。デフォルトはALLOWです。

from django.contrib import admin


class MyModelAdmin(admin.ModelAdmin):
    ...
    # Have facets always shown for this model admin.
    show_facets = admin.ShowFacets.ALWAYS

絞り込み件数が表示されることで、管理画面がより便利になりますね。

Simplified templates for form field rendering

DjangoのFormクラスを使ってテンプレートに入力フィールドを個別に記述する際、as_field_groupを使ってシンプルに書けるようになりました。

以下のNameFormを例に説明します。

from django import forms


class NameForm(forms.Form):
    name = forms.CharField(
        label="Your name",
        max_length=100,
        help_text="100 characters max.",
    )

nameの入力フィールドを表示する場合、通常はラベルやヘルプ文言なども合わせて表示するでしょう。そのため、テンプレートでは以下のように書く必要があります。

<form>
...
<div>
  {{ form.name.label_tag }}
  {% if form.name.help_text %}
    <div class="helptext" id="{{ form.name.auto_id }}_helptext">
      {{ form.name.help_text|safe }}
    </div>
  {% endif %}
  {{ form.name.errors }}
  {{ form.name }}
</div>
...
</form>

Django 5.0から追加されたas_field_groupを使うことで、上記の内容を以下のように書けます。

<form>
...
<div>
  {{ form.name.as_field_group }}
</div>
...
</form>

とてもシンプルになりましたね。

なお、as_field_groupでのレンダリングに使われるテンプレートはデフォルトではdjango/forms/field.htmlですが、これはカスタマイズ可能です。これまでは特定のフィールドをカスタマイズ表示したい場合、書かないといけない行数が多く面倒でしたが、 as_field_groupを使えばシンプルに記述できそうですね。

Database-computed default values

モデルフィールドのオプションにdb_defaultが追加され、データベースレベルでデフォルト値を設定できるようになりました。

もともとデフォルト値を設定するオプションとしてdefaultがありましたが、こちらはPythonで処理されるものであり、データベースレベルでのデフォルト値ではありませんでした。

比較のため、以下のようなMyModeldb_defaultを使用)とMyModel2defaultを使用)を定義し、マイグレーションファイルを作成します。そしてsqlmigrateコマンドでDDLを確認してみましょう。

from django.db import models
from django.db.models.functions import Now, Pi
from django.utils import timezone


class MyModel(models.Model):
    age = models.IntegerField(db_default=18)
    created = models.DateTimeField(db_default=Now())
    circumference = models.FloatField(db_default=2 * Pi())


def circumference_default():
    return 2 * math.pi


class MyModel2(models.Model):
    age = models.IntegerField(default=18)
    created = models.DateTimeField(default=timezone.now)
    circumference = models.FloatField(default=circumference_default)

マイグレーションファイルから生成されるDDLは以下です(データベースはSQLite3)。
見やすさのため整形しています。

BEGIN;
--
-- Create model MyModel
--
CREATE TABLE "sample_mymodel"(
    "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
    "age" integer DEFAULT 18 NOT NULL,
    "created" datetime DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')) NOT NULL,
    "circumference" real DEFAULT((2 * PI())) NOT NULL
);
--
-- Create model MyModel2
--
CREATE TABLE "sample_mymodel2"(
    "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
    "age" integer NOT NULL,
    "created" datetime NOT NULL,
    "circumference" real NOT NULL
);
COMMIT;

db_defaultを使用したMyModelは、DDLにデフォルト制約(DEFAULT)が付与されることが確認できました。

データベースレベルでデフォルト値が設定できるようになったことで、より強固なデフォルト制約をかけられます。

Database generated model field

モデルフィールドに新しくGeneratedFieldが追加され、Djangoのモデルで「生成列」を定義できるようになりました。生成列は、常に他のカラムから計算される列です。
たとえば以下のareaフィールドは、sideフィールドの値から計算されます。

from django.db import models
from django.db.models import F


class Square(models.Model):
    side = models.IntegerField()
    area = models.GeneratedField(
        expression=F("side") * F("side"),
        output_field=models.BigIntegerField(),
        db_persist=True,
    )

こちらも、マイグレーションファイル作成後、sqlmigrateコマンドでDDLを確認してみます。

BEGIN;
--
-- Create model Square
--
CREATE TABLE "sample_square"(
    "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
    "side" integer NOT NULL,
    "area" bigint GENERATED ALWAYS AS(("side" * "side")) STORED
);
COMMIT;

GENERATED ALWAYS ASにより、生成列が定義されていることが確認できました。
これまでも、以下のように@propertyを使って似たようなことはできました。

class Square2(models.Model):
    side = models.IntegerField()
    
    @property
    def area(self):
        return self.side ** 2

しかし、@propertyで定義した項目はクエリセットでは利用できません。

# エラーになる
Square2.objects.filter(area__gt=10)

Django 5.0 で追加されたGeneratedFieldを使って定義したフィールドであれば、クエリセットでも利用できます。

# エラーにならない
Square.objects.filter(area__gt=10)

More options for declaring field choices

モデルフィールドのchoicesオプションと、フォームフィールド ChoiceFieldchoicesオプションの設定が、より書きやすくなりました。
具体例として、以下の2つのモデルを比較してみましょう。

まず、以下はDjango 4.2までの書き方です。

from django.db import models

Medal = models.TextChoices("Medal", "GOLD SILVER BRONZE")

SPORT_CHOICES = [
    ("Martial Arts", [("judo", "Judo"), ("karate", "Karate")]),
    ("Racket", [("badminton", "Badminton"), ("tennis", "Tennis")]),
    ("unknown", "Unknown"),
]


class Winner(models.Model):
    name = models.CharField(...)
    medal = models.CharField(..., choices=Medal.choices)
    sport = models.CharField(..., choices=SPORT_CHOICES)

次に、以下はDjango 5.0での書き方です。

from django.db import models

Medal = models.TextChoices("Medal", "GOLD SILVER BRONZE")

SPORT_CHOICES = {  # Using a mapping instead of a list of 2-tuples.
    "Martial Arts": {"judo": "Judo", "karate": "Karate"},
    "Racket": {"badminton": "Badminton", "tennis": "Tennis"},
    "unknown": "Unknown",
}


def get_scores():
    return [(i, str(i)) for i in range(10)]


class Winner(models.Model):
    name = models.CharField(...)
    medal = models.CharField(..., choices=Medal)  # Using `.choices` not required.
    sport = models.CharField(..., choices=SPORT_CHOICES)
    score = models.IntegerField(choices=get_scores)  # A callable is allowed.

1点目は、列挙型であるMedalchoicesに指定する際、Medal.choicesではなくMedalと書けるようになりました。

2点目は、choicesに設定する「値と表示名の組み合わせ」を、タプルのリストではなく辞書で書けるようになりました。SPORT_CHOICESを比較してみてください。辞書のほうがより分かりやすいと思います。

3点目は、choicesに呼び出し可能オブジェクトを指定できるようになりました。上記の例ではget_scoresを指定しています。これを使うことで、choicesの設定値を動的に生成できます。

まとめ

Django 5.0 の主な新機能について紹介しました。
特にデータベース関連の新機能は、今後の開発で活用したいと思いました。
これからDjangoで新規開発するのであれば、まずは Django 5.0 の使用を検討するとよいと思います。
なお、Django 5.0 には他にも多くの変更がありますので、詳しくは Django 5.0のリリースノート をご確認ください。

Copyright ©2017- BeProud Inc. All rights reserved.