## 概要 以下の続き。プロジェクトもそのまま利用する。 <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 ```