目的
- Django REST Frameworkのちょっとしたチュートリアル、またはDjangoRESTMutipleModelsのExampleとして参照いただけると幸いです。
- またこちらにソースコードを用意しました。ご自由にどうぞ。
ユースケース
-
Django REST Frameworkをつかって簡単なECサイトを開発しているとします。
-
プロジェクトの構成は次のようにします。
1, 商品のモデルを定義しているitemアプリケーション
2, クーポンのモデルを定義しているcouponアプリケーション
3, WebAPIのエンドポイントとレスポンスデータについて定義しているapiv1アプリケーション
project
├── apiv1
├── config
├── item
├── coupon
- さらに同時にクライアントで商品一覧ページを作っているとします。
- 同ページでは商品の一覧に加えて、使用可能なクーポンの一覧をレスポンスするという仕様を考えています。
- クライアントからAPIサーバに一度に何回もリクエストを送るのではなく、商品一覧ページ用のエンドポイントを作って、1回のリクエストで両データを取得できるようにしたい。
- したがって最終的なレスポンスボディは以下のようになるように目指します。
{
"items": [
{},
{}
],
"coupons": [
{}
]
}
方針
- フィールドとしてモデルシリアライザをネストさせる方法がまず頭に浮かびましたが、公式のAPI Guideのページのいっちばん下によさげなパッケージを見つけました。
- 今回はガイドに従ってDjangoRESTMutipleModelsを使用して実装していきます。
- インストール方法は上記URLのREADMEを参照してください。
Modelの定義
- 商品モデルの定義です。
## item/models.py
from django.db import models
class Item(models.Model):
name = models.CharField(max_length=100)
price = models.IntegerField()
description = models.TextField()
- つづいてクーポンモデルの定義です。
# coupon/modesl.py
import datetime
from django.db import models
from django.utils import timezone
from django.utils.timezone import make_aware
class Coupon(models.Model):
name = models.CharField(max_length=100)
description = models.TextField()
# naiveタイムからawareタイムにmake_awareで変換
start_time = models.DateTimeField(default=make_aware(datetime.datetime.min))
end_time = models.DateTimeField(default=make_aware(datetime.datetime.max))
@property
def is_available(self):
now = timezone.now()
return self.start_time < now < self.end_time
@classmethod
def get_available_coupons(cls):
return [coupon for coupon in cls.objects.all() if coupon.is_available]
- クーポンの有効期限をstart_timeとend_timeフィールドから計算するis_availableプロパティで表現しました。
- start_timeとend_timeはデフォルト値でそれぞれdatetime.datetime.min、datetime.datetime.maxのawaitタイムを指定しています。理由としては、nullを使わずに遠い過去、遠い未来をデフォルト値に設定することで、is_availableプロパティの実装がnullを考慮する必要がなくなって簡単になるためです。
- awaitタイム、naiveタイムの違いはタイムゾーンを含んでいるかどうかの違いです。djangoORMで登録するにはawareタイム(タイムゾーンを含む)でないとwarningが出ます。
- 使用可能なクーポンだけにフィルタリングする機能は再利用される雰囲気を感じたのでクラスメソッドを用意しました。QuerySetクラスを作ってモデルマネージャにメソッドを定義したいところですが、QuerySetはSQLを発行するようなフィルタリングしか書くことができない点に注意です。
https://forum.djangoproject.com/t/how-to-filter-model-property-in-views/11869/11
Serializerの定義
- モデルシリアライザを定義しています。レスポンスデータにはすべてのカラムを含めることにします。
# apiv1.serializers.py
from rest_framework import serializers
from coupon.models import Coupon
from item.models import Item
class CouponSerializer(serializers.ModelSerializer):
class Meta:
model = Coupon
fields = '__all__'
class ItemSerializer(serializers.ModelSerializer):
class Meta:
model = Item
fields = '__all__'
Viewの定義
- いよいよviewsモジュールを実装します。ItemListAPIViewが商品一覧ページのためのビュークラスです。
# apiv1.views.py
from drf_multiple_model.views import ObjectMultipleModelAPIView
from coupon.models import Coupon
from item.models import Item
from apiv1.serializers import CouponSerializer, ItemSerializer
class ItemListAPIView(ObjectMultipleModelAPIView):
# 商品の一覧と
# 利用できるクーポン一覧
querylist = [
{
'queryset': Item.objects.all(),
'serializer_class': ItemSerializer,
'label': 'items',
},
{
'queryset': Coupon.get_available_coupons(),
'serializer_class': CouponSerializer,
'label': 'coupons'
},
]
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
- とはいっても、Django REST FrameworkのGenericAPIViewの制約に従って、queryset, seirlizer_classを辞書型のリストのような形式で定義するだけです。
- labelはそのままレスポンスデータのキーになります。
- アクションメソッドごとに、値を変えるときなど、get_querylistメソッドをオーバーライドする方法も公式ドキュメントには載っています。
- 思ったよりも簡単に実装ができてしまいましたが以上で終了です。お付き合いいただきありがとうございました。