これはQiita Django Advent Calendar 2023 11日目の記事です。
2023年12月4日、Django 5.0がリリースされました。主な変更点について解説します。
Django 5.0リリースおめでとう!(Created with DALL-E)
公式サイトでのリリース情報は以下を参照してください。
Django 5.0はlong-term support(LTS)版ではありません。サポート期限は2025年4月です。
LTS版である4.2のサポート期限(2026年4月)より短いため、バージョンアップするメリットがあるのか慎重に検討してください。
各バージョンのサポート期限についての詳細は以下公式ドキュメント「Supported Versions」を参照してください。
Facet filters in the admin
adminのフィルターに該当件数(ファセット数)を表示できるようになりました。ModelAdmin.show_facets属性で設定を変更できます。以下が設定例です。
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の画面は以下のように表示されます。
ファセット数を表示させると別途クエリが発行されるので、パフォーマンスに影響を及ぼす可能性があることに注意してください。
ModelAdmin.show_facets
属性に設定できる値は以下の3種類です1。
-
admin.ShowFacets.ALWAYS
: 常にファセット数を表示 -
admin.ShowFacets.ALLOW
: ファセット数の表示・非表示を切り替えられる(後述) -
admin.ShowFacets.NEVER
: ファセット数を表示させない
admin.ShowFacets.ALLOW
は以下のリンクが表示されて、ファセット数の表示・非表示を切り替えられます。
Simplified templates for form field rendering
独自のフォームフィールドテンプレートを作成する際の書き方がよりシンプルになりました。
まず、Django 4.2での書き方を見てください。色々なメソッドを組み合わせる必要があるため、かなり複雑な書き方にする必要があります。
<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()メソッドを使ってようにシンプルに書けます。以下の書き方は前述のテンプレートと同じ意味です。
<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属性が追加されました。従来のField.default属性と違って、データベース側で計算した値を設定できます。
以下がコード例です。
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())
上記モデルを使ってデータを登録すると以下のような結果になります。
>>> 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
属性は以下のように他のフィールドを参照する値を指定できません。
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
コマンドを実行すると、以下のエラーが発生します。
$ 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が追加されました。
以下の例ではfirst_name
とlast_name
を参照した値を生成するfull_name
というフィールドにGeneratedField
を使っています。
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
も更新されます。
>>> 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を使っています)。
$ 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属性、ChoiceField.choices属性に指定できる値の種類が2つ増えました。
1つ目は、列挙型を指定する際、列挙型の.choices
属性を使わず列挙型を直接指定できるようになりました。
まず、Django 4.2までの列挙型の使い方を見てください。
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からは以下のように書けます。
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なオブジェクトも渡せます。
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",
)