## 概要
以下の続き。プロジェクトもそのまま利用する。
<div class="link-card-v2">
<div class="link-card-v2-site">
<img class="link-card-v2-site-icon" src="https://publish-01.obsidian.md/access/35d05cd1bf5cc500e11cc8ba57daaf88/favicon-64.png" />
<span class="link-card-v2-site-name">Minerva</span>
</div>
<div class="link-card-v2-title">
📜2025-09-24 Django REST frameworkを学習してみる
</div>
<div class="link-card-v2-content">仕事でDjango REST frameworkを使う必要が生じ、機能が多く把握しきれなかったため、Django 4.2とDRF 3.16.1を用いて体系的な学習を開始した。環境構築では依存関係やバージョン選定、PostgreSQL導入、psycopg3の利用などに取り組み、SerializerやModelSerializerの実装、APIエンドポイント作成、Brunoによる動作確認までを段階的に実施した。</div>
<img class="link-card-v2-image" src="https://publish-01.obsidian.md/access/35d05cd1bf5cc500e11cc8ba57daaf88/Notes/attachments/activity.webp" />
<a data-href="📜2025-09-24 Django REST frameworkを学習してみる" class="internal-link"></a>
</div>
%%[[📜2025-09-24 Django REST frameworkを学習してみる]]%%
## 型定義の追加
### [[djangorestframework-stubs]]
[[Django REST framework]]の型定義がないので[[djangorestframework-stubs]]を追加する。
<div class="link-card-v2">
<div class="link-card-v2-site">
<img class="link-card-v2-site-icon" src="https://github.githubassets.com/favicons/favicon.svg" />
<span class="link-card-v2-site-name">GitHub</span>
</div>
<div class="link-card-v2-title">
GitHub - typeddjango/djangorestframework-stubs: PEP-484 stubs for django-rest-framework
</div>
<div class="link-card-v2-content">
PEP-484 stubs for django-rest-framework. Contribute to typeddjango/djangorestframework-stubs development by crea ...
</div>
<img class="link-card-v2-image" src="https://opengraph.githubassets.com/ab122a0add9cea29fbe612f14c6599cd40489269de8d7e8b0846f627e717746d/typeddjango/djangorestframework-stubs" />
<a href="https://github.com/typeddjango/djangorestframework-stubs"></a>
</div>
```console
$ uv add djangorestframework-stubs
Installed 7 packages in 9ms
+ certifi==2025.10.5
+ charset-normalizer==3.4.3
+ djangorestframework-stubs==3.16.4
+ idna==3.10
+ requests==2.32.5
+ types-requests==2.32.4.20250913
+ urllib3==2.5.0
```
ただ、変化がなさそう。[[djangorestframework-stubs]]は[[mypy]]専用であり、[[mypy]]を使わず[[Pyright]]を使っているためな気がする。
### [[djangorestframework-types]]
[[djangorestframework-types]]を試してみる。
<div class="link-card-v2">
<div class="link-card-v2-site">
<img class="link-card-v2-site-icon" src="https://github.githubassets.com/favicons/favicon.svg" />
<span class="link-card-v2-site-name">GitHub</span>
</div>
<div class="link-card-v2-title">
GitHub - sbdchd/djangorestframework-types: :sunflower: Type stubs for Django Rest Framework
</div>
<div class="link-card-v2-content">
:sunflower: Type stubs for Django Rest Framework. Contribute to sbdchd/djangorestframework-types development by ...
</div>
<img class="link-card-v2-image" src="https://opengraph.githubassets.com/75419a76f20abaa525ac2b6aabd1bf1e4ac2fe2ee288d279f89edd502f190547/sbdchd/djangorestframework-types" />
<a href="https://github.com/sbdchd/djangorestframework-types"></a>
</div>
> Note: this project was forked from [https://github.com/typeddjango/djangorestframework-stubs](https://github.com/typeddjango/djangorestframework-stubs) with the goal of removing the [`mypy`](https://github.com/python/mypy) plugin dependency so that `mypy` can't [crash due to Django config](https://github.com/typeddjango/django-stubs/issues/318), and that non-`mypy` type checkers like [`pyright`](https://github.com/microsoft/pyright) will work better with Django Rest Framework.
>
> *[sbdchd/djangorestframework-types: :sunflower: Type stubs for Django Rest Framework](https://github.com/sbdchd/djangorestframework-types)*
とあるので期待できる。[[djangorestframework-stubs]]はまず削除。
```console
uv remove djangorestframework-stubs
```
[[djangorestframework-types]]をインストール。
```console
$ uv add djangorestframework-types
Installed 1 package in 3ms
+ djangorestframework-types==0.9.0
```
ただ、予期せぬ型エラーが発生してしまったのでやめた。
### パッケージのインストールは不要だった
もともとは `Request` を解決したかったのだが
```python
@csrf_exempt
def animal_list(request: Request):
```
以下のimportに普通に解決できた。
```python
from rest_framework.request import Request
```
## リクエストとレスポンス
2つ目のチュートリアルに入る。
<div class="link-card-v2">
<div class="link-card-v2-site">
<img class="link-card-v2-site-icon" src="https://www.django-rest-framework.org/img/favicon.ico" />
<span class="link-card-v2-site-name">www.django-rest-framework.org</span>
</div>
<div class="link-card-v2-title">
2 - Requests and responses - Django REST framework
</div>
<div class="link-card-v2-content">
Django, API, REST, 2 - Requests and responses
</div>
<a href="https://www.django-rest-framework.org/tutorial/2-requests-and-responses/"></a>
</div>
- `request.data` は `POST` だけでなく `PUT` `PATCH` でも利用できる
- `Response(...)` は [[Content-Type]] に応じてデータを変換する
ステータスコードは可読性が高いLiteral定義を使う。
```python
from rest_framework.status import (
HTTP_201_CREATED,
HTTP_204_NO_CONTENT,
HTTP_400_BAD_REQUEST,
HTTP_404_NOT_FOUND,
HTTP_405_METHOD_NOT_ALLOWED,
)
```
### リファクタリング
上記と `@api_view` を踏まえてリファクタリングする。
`zoo/animals/views.py`
```python
from rest_framework.decorators import api_view
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.status import (
HTTP_201_CREATED,
HTTP_204_NO_CONTENT,
HTTP_400_BAD_REQUEST,
HTTP_404_NOT_FOUND,
)
from zoo.animals.models import Animal
from zoo.animals.serializers import AnimalSerializer
# @csrf_exempt -> @api_view
@api_view(["GET", "POST"])
def animal_list(request: Request):
if request.method == "GET":
animals = Animal.objects.all()
serializer = AnimalSerializer(animals, many=True)
# JsonResponse -> Response
return Response(serializer.data)
if request.method == "POST":
# data = JSONParser().parse(request) -> request.data
serializer = AnimalSerializer(data=request.data)
if not serializer.is_valid():
# JsonResponse -> Response
return Response(serializer.errors, status=HTTP_400_BAD_REQUEST)
serializer.save()
# JsonResponse -> Response
return Response(serializer.data, status=HTTP_201_CREATED)
# 405の例外処理を削除
# @csrf_exempt -> @api_view
@api_view(["GET", "PUT", "DELETE"])
def animal_detail(request: Request, pk):
try:
animal = Animal.objects.get(pk=pk)
except Animal.DoesNotExist:
# HttpResponse -> Response
return Response(status=HTTP_404_NOT_FOUND)
if request.method == "GET":
serializer = AnimalSerializer(animal)
# JsonResponse -> Response
return Response(serializer.data)
if request.method == "PUT":
# data = JSONParser().parse(request) -> request.data
serializer = AnimalSerializer(animal, data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=HTTP_400_BAD_REQUEST)
serializer.save()
# JsonResponse -> Response
return Response(serializer.data)
if request.method == "DELETE":
animal.delete()
# HttpResponse -> Response
return Response(status=HTTP_204_NO_CONTENT)
```
- リクエストは `request.data`
- レスポンスは `Response(...)`
ですべて吸収できるのが強い。
### ブラウザからのリクエスト
以前はブラウザで http://localhost:8000/animals/ にリクエストすると[[JSON]]を返していたが、今は[[HTML]]が返却される。これはブラウザの[[Accept]]ヘッダーで `text/html` が送信されており、リファクタリングしたあとのコードがそれを加味して[[HTML]]形式のレスポンスを返却しているから。
## クラスベースのViews
3つ目のチュートリアルへ。
<div class="link-card-v2">
<div class="link-card-v2-site">
<img class="link-card-v2-site-icon" src="https://www.django-rest-framework.org/img/favicon.ico" />
<span class="link-card-v2-site-name">www.django-rest-framework.org</span>
</div>
<div class="link-card-v2-title">
3 - Class based views - Django REST framework
</div>
<div class="link-card-v2-content">
Django, API, REST, 3 - Class based views
</div>
<a href="https://www.django-rest-framework.org/tutorial/3-class-based-views/"></a>
</div>
### クラスの導入
`views.py` を[[クラス (Python)|クラス]]を使って書き直す。
`zoo/animals/views.py`
```python
from django.http import Http404
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.status import (
HTTP_201_CREATED,
HTTP_204_NO_CONTENT,
HTTP_400_BAD_REQUEST,
)
from rest_framework.views import APIView
from zoo.animals.models import Animal
from zoo.animals.serializers import AnimalSerializer
class AnimalList(APIView):
def get(self, request: Request):
animals = Animal.objects.all()
serializer = AnimalSerializer(animals, many=True)
return Response(serializer.data)
def post(self, request: Request):
serializer = AnimalSerializer(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=HTTP_400_BAD_REQUEST)
serializer.save()
return Response(serializer.data, status=HTTP_201_CREATED)
class AnimalDetail(APIView):
def get_object(self, pk):
try:
return Animal.objects.get(pk=pk)
except Animal.DoesNotExist:
raise Http404
def get(self, request: Request, pk):
animal = self.get_object(pk)
serializer = AnimalSerializer(animal)
return Response(serializer.data)
def put(self, request: Request, pk):
animal = self.get_object(pk)
serializer = AnimalSerializer(animal, data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=HTTP_400_BAD_REQUEST)
serializer.save()
return Response(serializer.data)
def delete(self, request: Request, pk):
animal = self.get_object(pk)
animal.delete()
return Response(status=HTTP_204_NO_CONTENT)
```
`zoo/animals/urls.py`
```python
from django.urls import path
from zoo.animals import views
urlpatterns = [
path("animals/", views.AnimalList.as_view()),
path("animals/<uuid:pk>/", views.AnimalDetail.as_view()),
]
```
### Mixinの導入
各種Mixinを導入して、メソッドのロジックを委譲する。代わりに [[QuerySet (Django)|QuerySet]] と `serializer_class` を別途指定する必要がある。
`zoo/animals/views.py`
```python
from rest_framework import generics, mixins
from rest_framework.request import Request
from zoo.animals.models import Animal
from zoo.animals.serializers import AnimalSerializer
class AnimalList(
mixins.ListModelMixin, # self.list を追加
mixins.CreateModelMixin, # self.create を追加
generics.GenericAPIView,
):
queryset = Animal.objects.all()
serializer_class = AnimalSerializer
def get(self, request: Request, *args, **kwargs):
return self.list(request, *args, **kwargs)
def post(self, request: Request, *args, **kwargs):
return self.create(request, *args, **kwargs)
class AnimalDetail(
mixins.RetrieveModelMixin, # self.retrieve を追加
mixins.UpdateModelMixin, # self.update を追加
mixins.DestroyModelMixin, # self.destroy を追加
generics.GenericAPIView,
):
queryset = Animal.objects.all()
serializer_class = AnimalSerializer
def get(self, request: Request, *args, **kwargs):
return self.retrieve(request, *args, **kwargs)
def put(self, request: Request, *args, **kwargs):
return self.update(request, *args, **kwargs)
def delete(self, request: Request, *args, **kwargs):
return self.destroy(request, *args, **kwargs)
```
各Mixinの中身で呼び出している[[メソッド (Python)|メソッド]]は `generics.GenericAPIView` に定義されたものなので、Mixinの定義内には存在しない。
### より抽象化されたクラス
お決まりのパターンであれば継承で[[メソッド (Python)|メソッド]]定義を省略できる。詳細は `rest_framework.generics` の定義を参照。
`zoo/animals/views.py`
```python
from rest_framework import generics
from zoo.animals.models import Animal
from zoo.animals.serializers import AnimalSerializer
class AnimalList(generics.ListCreateAPIView):
queryset = Animal.objects.all()
serializer_class = AnimalSerializer
class AnimalDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Animal.objects.all()
serializer_class = AnimalSerializer
```
## 認証と権限
4つ目のチュートリアルへ。
<div class="link-card-v2">
<div class="link-card-v2-site">
<img class="link-card-v2-site-icon" src="https://www.django-rest-framework.org/img/favicon.ico" />
<span class="link-card-v2-site-name">www.django-rest-framework.org</span>
</div>
<div class="link-card-v2-title">
4 - Authentication and permissions - Django REST framework
</div>
<div class="link-card-v2-content">
Django, API, REST, 4 - Authentication and permissions
</div>
<a href="https://www.django-rest-framework.org/tutorial/4-authentication-and-permissions/"></a>
</div>
今回満たすべき要件。
| ユーザーの種類 | 取得 | 登録 | 更新 | 削除 |
| ------------ | --- | --- | --- | --- |
| データ作成者 | O | O | O | O |
| 認証されたユーザー | O | O | X | X |
| 認証されていないユーザー | O | X | X | X |
### Modelに作成者を追加
`zoo/animals/models.py`
```python
from django.db import models
from rest_framework.fields import uuid
ANIMAL_KIND = (
("dog", "犬"),
("cat", "猫"),
("owl", "フクロウ"),
("gorilla", "ゴリラ"),
)
class Animal(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=100, blank=True, default="")
description = models.TextField()
proper = models.BooleanField(default=False)
kind = models.CharField(choices=ANIMAL_KIND, default="owl", max_length=32)
created = models.DateTimeField(auto_now_add=True)
# 追加
owner = models.ForeignKey(
"auth.User", related_name="animals", on_delete=models.CASCADE
)
class Meta:
ordering = ["created"]
```
#### マイグレーション
マイグレーションを実行すると `non-nullable` フィールドの既存レコードをどう扱うかを求められる。
```console
$ python manage.py makemigrations animals
It is impossible to add a non-nullable field 'owner' to animal without specifying a default. This is because the database needs something to populate existing rows.
Please select a fix:
1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
2) Quit and manually define a default value in models.py.
```
一度DBを空にしたほうが良いので、そうしてから再実行する。コンテナを作り直す。
```console
docker compose down && docker compose up -d
```
と思ったけどメッセージは変わらない。DBの実体がどうこうよりもマイグレーションファイルがその可能性を残しているから表示されるメッセージっぽい。一度マイグレーションファイルをクリアして作り直す。
```console
rm -rf zoo/animals/migrations/
```
もう一度チャレンジ。
```console
python manage.py makemigrations animals
python manage.py migrate
```
`auth_user` としっかり関連ができてそう。
![[2025-10-12-17-10-16.avif]]
#### ユーザーを追加
ユーザーを追加。今回は数人必要なので何人か。
```console
python manage.py createsuperuser --username ミネルバ --email
[email protected]
python manage.py createsuperuser --username オブシディアン --email
[email protected]
python manage.py createsuperuser --username ネオちゃん --email
[email protected]
```
- ミネルバ
- オブシディアン
- ネオちゃん
### Serializerに追加
`zoo/animals/serializers.py`
```python
from django.contrib.auth.models import User
from rest_framework import serializers
from zoo.animals.models import Animal
# 追加
class UserSerializer(serializers.ModelSerializer):
# animalsはAnimalモデルからの逆参照なので定義が必要
animals = serializers.PrimaryKeyRelatedField(
many=True, queryset=Animal.objects.all()
)
class Meta:
model = User
# idとusernameはUserに含まれているが、animalsは含まれていない
fields = ["id", "username", "animals"]
class AnimalSerializer(serializers.ModelSerializer):
class Meta:
model = Animal
fields = "__all__"
```
### Viewに追加
`zoo/animals/views.py`
```python
from django.contrib.auth.models import User
from rest_framework import generics
from zoo.animals.models import Animal
from zoo.animals.serializers import AnimalSerializer, UserSerializer
# 追加
class UserList(generics.ListCreateAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
# 追加
class UserDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
class AnimalList(generics.ListCreateAPIView):
queryset = Animal.objects.all()
serializer_class = AnimalSerializer
class AnimalDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Animal.objects.all()
serializer_class = AnimalSerializer
```
### Urlに追加
`zoo/animals/urls.py`
```python
from django.urls import path
from zoo.animals import views
urlpatterns = [
# 追加
path("users/", views.UserList.as_view()),
# 追加
path("users/<int:pk>/", views.UserDetail.as_view()),
path("animals/", views.AnimalList.as_view()),
path("animals/<uuid:pk>/", views.AnimalDetail.as_view()),
]
```
### 取得確認
http://localhost:8000/users/ でユーザー一覧を取得できる。
```json
[
{
"id": 1,
"username": "ミネルバ",
"animals": []
},
{
"id": 2,
"username": "オブシディアン",
"animals": []
},
{
"id": 3,
"username": "ネオちゃん",
"animals": []
}
]
```
ただ `POST http://localhost:8000/animals/` を実行しても400エラーになる。
```json
{
"owner": [
"This field is required."
]
}
```
これは `owner` に認証されたユーザー情報が渡っていないためである。
### 認証されたユーザー情報を渡す
`perfirm_create()` を定義する。`zoo/animals/views.py` の `AnimalList` 変更部分だけを記述。
```python
class AnimalList(generics.ListCreateAPIView):
queryset = Animal.objects.all()
serializer_class = AnimalSerializer
# 追加
def perform_create(self, serializer: AnimalSerializer):
serializer.save(owner=self.request.user)
```
`zoo/animals/serializers.py` の `AnimalSerializer` クラスにも `owner` を追加する。
```python
class AnimalSerializer(serializers.ModelSerializer):
# 取得時のみ owner に owner.username の値をセット
owner = serializers.ReadOnlyField(source="owner.username")
class Meta:
model = Animal
fields = "__all__"
```
`ReadOnlyField` なので、更新時にこのパラメータは利用されない。(指定する必要はない)
### 権限による認証設定を追加する
`zoo/animals/views.py` の `AnimalList` と `AnimalDetail` に `permissions.IsAuthenticatedOrReadOnly` を追加する。
```python
class AnimalList(generics.ListCreateAPIView):
queryset = Animal.objects.all()
serializer_class = AnimalSerializer
# 追加: 参照系は許可、更新系は認証済みユーザーのみ許可
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
def perform_create(self, serializer: AnimalSerializer):
serializer.save(owner=self.request.user)
class AnimalDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Animal.objects.all()
serializer_class = AnimalSerializer
# 追加: 参照系は許可、更新系は認証済みユーザーのみ許可
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
```
### 認証する
認証ユーザーによる権限制御をテストするため、認証されたユーザーとしてリクエストできるようにする必要がある。
> We haven't set up any [authentication classes](https://www.django-rest-framework.org/api-guide/authentication/), so the defaults are currently applied, which are `SessionAuthentication` and `BasicAuthentication`.
[[Basic認証]]はデフォルトで使えそうなのでこれを利用する。`e2e/collection.bru` を追加して認証情報を記載する。
```
auth {
mode: basic
}
auth:basic {
username: ミネルバ
password: password
}
```
> [!danger]
> ローカル以外で利用する漏洩したらまずいパスワードは使わないこと。
`POST http://localhost:8000/animals/` で以下をBodyとして送信。
```json
{
"name": "みみこ",
"description": "みみぞうの妹",
"proper": false,
"kind": "owl"
}
```
結果を得られる。
```json
{
"id": "093eeeb5-6a27-433f-9952-21fdc9445862",
"owner": "ミネルバ",
"name": "みみこ",
"description": "みみぞうの妹",
"proper": false,
"kind": "owl",
"created": "2025-10-12T09:04:41.953788Z"
}
```
Authを無効にすると403エラーになる。
```json
{
"detail": "Authentication credentials were not provided."
}
```
### 更新・削除の権限を制御する
認証されたユーザーであれば誰でも『更新・削除』できる状態になってしまっているので、`owner` 以外はできないように制御する必要がある。
`zoo/animals/permissions.py`
```python
from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
"""
オブジェクトの所有者にのみ編集権限を与え、その他のユーザーには読み取り専用のアクセスを許可するカスタムパーミッション。
"""
def has_object_permission(self, request, view, obj):
# 読み取り専用のリクエスト(GET, HEAD, OPTIONS)は常に許可
if request.method in permissions.SAFE_METHODS:
return True
# 書き込みリクエストの場合、オブジェクトの所有者であることを確認
return obj.owner == request.user
```
`IsOwnerOrReadOnly` を `AnimalDetail` viewの `permission_classes` に追加する。
```python
# 関連箇所のみ記載
from zoo.animals.permissions import IsOwnerOrReadOnly
class AnimalDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Animal.objects.all()
serializer_class = AnimalSerializer
permission_classes = [
permissions.IsAuthenticatedOrReadOnly,
# 追加
IsOwnerOrReadOnly,
]
```
`owner` じゃないユーザーが更新・削除すると403エラーになることが確認できた。
```json
{
"detail": "You do not have permission to perform this action."
}
```
## ViewSetsとRouterの利用
チュートリアルの最終章。
<div class="link-card-v2">
<div class="link-card-v2-site">
<img class="link-card-v2-site-icon" src="https://www.django-rest-framework.org/img/favicon.ico" />
<span class="link-card-v2-site-name">www.django-rest-framework.org</span>
</div>
<div class="link-card-v2-title">
6 - Viewsets and routers - Django REST framework
</div>
<div class="link-card-v2-content">
Django, API, REST, 6 - Viewsets and routers
</div>
<a href="https://www.django-rest-framework.org/tutorial/6-viewsets-and-routers/"></a>
</div>
### ViewSetsへリファクタリング
`zoo/animals/views.py` の 以下をそれぞれViewSetsに書き換える。
- `UserList` + `UserDetail` -> `UserViewSet`
- `AnimalList` + `AnimalDetail` -> `AnimalViewSet`
```python
from django.contrib.auth.models import User
from rest_framework import permissions, viewsets
from zoo.animals.models import Animal
from zoo.animals.permissions import IsOwnerOrReadOnly
from zoo.animals.serializers import AnimalSerializer, UserSerializer
class UserViewSet(viewsets.ReadOnlyModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
class AnimalViewSet(viewsets.ModelViewSet):
queryset = Animal.objects.all()
serializer_class = AnimalSerializer
permission_classes = [
permissions.IsAuthenticatedOrReadOnly,
IsOwnerOrReadOnly,
]
def perform_create(self, serializer: AnimalSerializer):
serializer.save(owner=self.request.user)
```
正規のエンドポイント以外を作成する場合は `@action` を使う。
### Routerの適応
`urls.py` の記述をRouterでシンプルに書けることがViewSetのメリットの1つ。
`zoo/animals/urls.py`
```python
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from zoo.animals import views
router = DefaultRouter()
router.register(r"users", views.UserViewSet, basename="user")
router.register(r"animals", views.AnimalViewSet, basename="animal")
urlpatterns = [
path("", include(router.urls)),
]
```
## appendex
チュートリアルの内容はここまで。ここからはプラスアルファの内容。
### 関連モデルをオブジェクトとして返す
http://localhost:8000/users/ の結果は現状以下になっている。
```json
[
{
"id": 1,
"username": "ミネルバ",
"animals": [
"0861c5ce-8d3a-40dc-8fc0-c82898b105cf",
"c834a1f4-deb7-45cb-a9c9-9933a44289b2"
]
},
{
"id": 2,
"username": "オブシディアン",
"animals": []
},
{
"id": 3,
"username": "ネオちゃん",
"animals": []
}
]
```
`animals` はidの配列が返却されるが、これを実際の `Animal` オブジェクトにしたい。ネストしてSerializerを指定するだけでよい。(定義順は逆転している)
`zoo/animals/serializers.py`
```python
from django.contrib.auth.models import User
from rest_framework import serializers
from zoo.animals.models import Animal
class AnimalSerializer(serializers.ModelSerializer):
# 取得時のみ owner に owner.username の値をセット
owner = serializers.ReadOnlyField(source="owner.username")
class Meta:
model = Animal
fields = "__all__"
class UserSerializer(serializers.ModelSerializer):
animals = AnimalSerializer(many=True)
class Meta:
model = User
# idとusernameはUserに含まれているが、animalsは含まれていない
fields = ["id", "username", "animals"]
```
結果はこのように変わる。
```json
[
{
"id": 1,
"username": "ミネルバ",
"animals": [
{
"id": "0861c5ce-8d3a-40dc-8fc0-c82898b105cf",
"owner": "ミネルバ",
"name": "みみこ",
"description": "みみぞうの妹",
"proper": false,
"kind": "owl",
"created": "2025-10-12T09:33:32.582928Z"
},
{
"id": "c834a1f4-deb7-45cb-a9c9-9933a44289b2",
"owner": "ミネルバ",
"name": "みみたろう",
"description": "みみこの妹",
"proper": false,
"kind": "owl",
"created": "2025-10-12T09:33:37.229279Z"
}
]
},
{
"id": 2,
"username": "オブシディアン",
"animals": []
},
{
"id": 3,
"username": "ネオちゃん",
"animals": []
}
]
```
### 標準出力に[[SQL]]ログを出力する
`zoo/settings.py` に以下を追加。
```python
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"console": {
"class": "logging.StreamHandler",
},
},
"loggers": {
"django.db.backends": {
"handlers": ["console"],
"level": "DEBUG",
"propagate": False,
},
},
"root": {
"handlers": ["console"],
"level": "WARNING",
},
}
```
> [[SQLログを標準出力に出力 (Django)|SQLログを標準出力に出力]]
### [[N+1問題]]を解消する
今までのコードでは、以下のように[[N+1問題]]が発生する。1 + 3(N) = 4回のクエリが発行されている。
```sql
(0.002) SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user"; args=(); alias=default
(0.003) SELECT "animals_animal"."id", "animals_animal"."name", "animals_animal"."description", "animals_animal"."proper", "animals_animal"."kind", "animals_animal"."created", "animals_animal"."owner_id" FROM "animals_animal" WHERE "animals_animal"."owner_id" = 1 ORDER BY "animals_animal"."created" ASC; args=(1,); alias=default
(0.001) SELECT "animals_animal"."id", "animals_animal"."name", "animals_animal"."description", "animals_animal"."proper", "animals_animal"."kind", "animals_animal"."created", "animals_animal"."owner_id" FROM "animals_animal" WHERE "animals_animal"."owner_id" = 2 ORDER BY "animals_animal"."created" ASC; args=(2,); alias=default
(0.001) SELECT "animals_animal"."id", "animals_animal"."name", "animals_animal"."description", "animals_animal"."proper", "animals_animal"."kind", "animals_animal"."created", "animals_animal"."owner_id" FROM "animals_animal" WHERE "animals_animal"."owner_id" = 3 ORDER BY "animals_animal"."created" ASC; args=(3,); alias=default
```
`User:Animal` は `1:n` の関係なので [[select_related (Django)|select_related]]は使えない。[[prefetch_related (Django)|prefetch_related]]を使って[[QuerySet (Django)|QuerySet]]を書き換える。
```diff
class UserViewSet(viewsets.ReadOnlyModelViewSet):
- queryset = User.objects.all()
+ queryset = User.objects.prefetch_related("animals")
serializer_class = UserSerializer
```
ことで、クエリは 1 + 1 = 2回 の発行で済んでいる。
```sql
(0.003) SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user"; args=(); alias=default
(0.004) SELECT "animals_animal"."id", "animals_animal"."name", "animals_animal"."description", "animals_animal"."proper", "animals_animal"."kind", "animals_animal"."created", "animals_animal"."owner_id" FROM "animals_animal" WHERE "animals_animal"."owner_id" IN (1, 2, 3) ORDER BY "animals_animal"."created" ASC; args=(1, 2, 3); alias=default
```