最近Django REST Frameworkを触っています。
そういえば「fieldを指定したGETリクエストってどうやってやるんだっけ?」と気になったので調べてみました。
例によってブログです。
以下のようなシンプルな構成でテストしていきます。
from django.db import models
from django.utils import timezone
class Post(models.Model):
"""記事"""
title = models.CharField('タイトル', max_length=40)
description = models.TextField('紹介文')
main_text = models.TextField('本文')
created_at = models.DateTimeField('作成日', default=timezone.now)
def __str__(self):
return self.title
タイトルと詳細と本文と作成日という非常にシンプルな構成です。
続いてビューとシリアライザーをざっくりと書いていきます。
# serializers.py
from rest_framework import serializers
from .models import Post
class PostSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = '__all__'
あとはviews.pyでこのシリアライザーを指定します。
# views.py
from rest_framework import generics
from .models import Post
from .serializers import PostSerializer
class PostList(generics.ListAPIView):
queryset = Post.objects.all()
serializer_class = PostSerializer
簡易的ですが、記事一覧の取得APIができました。
開発サーバーを起動させてリクエストを送ってみましょう。
リクエストURLはhttp://127.0.0.1:8000/api/blog/posts/
としました。
方法は何でもいいんですが、requestsモジュールを使いました。
import requests
url = 'http://127.0.0.1:8000/api/blog/posts/'
r = requests.request('GET', url)
from pprint import pprint
pprint(r.json())
以下のように全てのフィールドが取得されます。
[{'created_at': '2022-05-15T08:17:01+09:00',
'description': 'test',
'id': 1,
'main_text': 'test',
'title': 'test'},
{'created_at': '2023-10-15T20:03:59+09:00',
'description': '柴田聡子の新曲について',
'id': 3,
'main_text': 'Synergyは最高だ。',
'title': '柴田聡子について'},
{'created_at': '2023-10-15T20:17:51+09:00',
'description': '柴田聡子の名曲、ラッキーカラーについての記事です',
'id': 4,
'main_text': 'たんたんとした愛がわからなくなってすぐに地図を見てる、という歌詞が印象に残った',
'title': 'ラッキーカラーについて'}]
ここからフィールドを指定できるようにしていきます。
例えばmain_textなどはブログの本文が入ることから、一覧ページでは取得の必要がない、といった要件はあると思います。
一つの方法としてはmain_textを除いた一覧用のシリアライザを作成するなどがあります。
今回は、リクエストをする際に動的に指定をできるようにしていきます。
まず以下のようにフィールドを動的に管理できるサブクラスシリアライザを作成します。
from rest_framework import serializers
from .models import Post
class DynamicFieldsModelSerializer(serializers.ModelSerializer):
"""
モデルシリアライザーのサブクラス
フィールドを動的に管理する
"""
def __init__(self, *args, **kwargs):
# インスタンス化時に 指定された'fields'の値を取得する
fields = kwargs.pop('fields', None)
super().__init__(*args, **kwargs)
if fields:
# 指定されていないfieldsをとりのぞく
allowed = set(fields)
existing = set(self.fields)
for field_name in existing:
# 指定されていない
if field_name not in allowed:
# とりのぞく
self.fields.pop(field_name)
# DynamicFieldsModelSerializerを継承
class PostSerializer(DynamicFieldsModelSerializer):
class Meta:
model = Post
fields = '__all__'
次にビュー側でリクエストがあった際に、指定されたfieldsの値をリスト化して、シリアライザをインスタンス化する…という風にします。
from rest_framework import generics
from .models import Post
from .serializers import PostSerializer
class PostList(generics.ListAPIView):
queryset = Post.objects.all()
serializer_class = PostSerializer
def get_serializer(self, *args, **kwargs):
"""
このビューで使用されるシリアライザーのインスタンスを返す
"""
serializer_class = self.get_serializer_class()
kwargs['context'] = self.get_serializer_context()
fields = self.request.query_params.get('fields')
if fields:
# リスト化してシリアライザに渡す
fields = fields.split(',')
kwargs['fields'] = fields
return serializer_class(*args, **kwargs)
呼び出すときは以下のように辞書形式でparamsを渡します。
import requests
url = 'http://127.0.0.1:8000/api/blog/posts/'
# idとtitleフィールドを指定
params = {'fields': 'id,title'}
r = requests.request('GET', url, params=params)
from pprint import pprint
pprint(r.json())
[{'id': 1, 'title': 'test'},
{'id': 3, 'title': '柴田聡子について'},
{'id': 4, 'title': 'ラッキーカラーについて'}]
期待している通りにidとtitleだけ取り出せました。
流れを振り返ると、クライアントはクエリパラメータfieldsを使って、レスポンスに含めてほしいフィールド名をカンマ区切りで指定します。
リクエストがDjangoアプリケーションのサーバーに到達すると、PostListビューがこれを処理します。
ビューのget_serializerメソッドが、リクエストからfieldsクエリパラメータを取得し、リストに変換します。
このリストは、後続の処理でシリアライザーに渡されるfields引数となります。
PostSerializer(DynamicFieldsModelSerializerを継承)がfields引数を受け取り、この引数に基づいてインスタンスのフィールドセットを動的に調整します。具体的には、fieldsに含まれない全てのフィールドを削除する処理をしています。
そうしてシリアライザが生成したデータがHTTPレスポンスとして、最終的にクライアントに送り返されました。
おまけで個別に指定するだけではなくて、特定のフィールド以外…という指定もできるようにしてみます。
class DynamicFieldsModelSerializer(serializers.ModelSerializer):
"""
モデルシリアライザーのサブクラス
フィールドを動的に管理する
"""
def __init__(self, *args, **kwargs):
fields = kwargs.pop('fields', None)
# 追加、同様に取得する
exclude = kwargs.pop('exclude', None)
super().__init__(*args, **kwargs)
if fields:
allowed = set(fields)
existing = set(self.fields)
for field_name in existing:
if field_name not in allowed:
self.fields.pop(field_name)
# 指定されたfieldを取り除く
if exclude:
for field_name in exclude:
self.fields.pop(field_name, None)
ビュー側にも同様に追加。
class PostList(generics.ListAPIView):
queryset = Post.objects.all()
serializer_class = PostSerializer
def get_serializer(self, *args, **kwargs):
"""
このビューで使用されるシリアライザーのインスタンスを返す
"""
serializer_class = self.get_serializer_class()
kwargs['context'] = self.get_serializer_context()
fields = self.request.query_params.get('fields')
# 追加
exclude = self.request.query_params.get('exclude')
if fields:
fields = fields.split(',')
kwargs['fields'] = fields
# 追加
if exclude:
exclude = exclude.split(',')
kwargs['exclude'] = exclude
return serializer_class(*args, **kwargs)
リクエストを送ってみましょう。
import requests
url = 'http://127.0.0.1:8000/api/blog/posts/'
# main_text以外を指定
params = {'exclude': 'main_text'}
r = requests.request('GET', url, params=params)
from pprint import pprint
pprint(r.json())
[{'created_at': '2022-05-15T08:17:01+09:00',
'description': 'test',
'id': 1,
'title': 'test'},
{'created_at': '2023-10-15T20:03:59+09:00',
'description': '柴田聡子の新曲について',
'id': 3,
'title': '柴田聡子について'},
{'created_at': '2023-10-15T20:17:51+09:00',
'description': '柴田聡子の名曲、ラッキーカラーについての記事です',
'id': 4,
'title': 'ラッキーカラーについて'}]
うまくmain_text以外のfieldを取得することができました。