Django REST Frameworkで複数のモデルインスタンスをシリアライズする

## 目的

- Django REST Frameworkのちょっとしたチュートリアル、またはDjangoRESTMutipleModelsのExampleとして参照いただけると幸いです。
- また[こちら](https://github.com/Koki-Sato-Daito/multiple_models_example/tree/main)にソースコードを用意しました。ご自由にどうぞ。



## ユースケース

- Django REST Frameworkをつかって簡単なECサイトを開発しているとします。

- プロジェクトの構成は次のようにします。

  1, 商品のモデルを定義しているitemアプリケーション

  2, クーポンのモデルを定義しているcouponアプリケーション

  3, WebAPIのエンドポイントとレスポンスデータについて定義しているapiv1アプリケーション

```
project
├── apiv1
├── config
├── item
├── coupon
```

- さらに同時にクライアントで商品一覧ページを作っているとします。
- 同ページでは商品の一覧に加えて、使用可能なクーポンの一覧をレスポンスするという仕様を考えています。
- クライアントからAPIサーバに一度に何回もリクエストを送るのではなく、商品一覧ページ用のエンドポイントを作って、１回のリクエストで両データを取得できるようにしたい。
- したがって最終的なレスポンスボディは以下のようになるように目指します。

```json
{
    "items": [
        {},
        {}
    ],
    "coupons": [
        {}
    ]
}
```





## 方針

- フィールドとしてモデルシリアライザをネストさせる方法がまず頭に浮かびましたが、[公式のAPI Guideのページのいっちばん下](https://www.django-rest-framework.org/api-guide/generic-views/#django-rest-multiple-models)によさげなパッケージを見つけました。
- 今回はガイドに従って[DjangoRESTMutipleModels](https://github.com/MattBroach/DjangoRestMultipleModels)を使用して実装していきます。
- インストール方法は上記URLのREADMEを参照してください。





## Modelの定義

- 商品モデルの定義です。

```python
## item/models.py

from django.db import models


class Item(models.Model):
    name = models.CharField(max_length=100)
    price = models.IntegerField()
    description = models.TextField()
```

- つづいてクーポンモデルの定義です。

```python
# 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の定義

- モデルシリアライザを定義しています。レスポンスデータにはすべてのカラムを含めることにします。

````python
# 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が商品一覧ページのためのビュークラスです。

```python
# 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メソッドをオーバーライドする方法も[公式ドキュメント](https://django-rest-multiple-models.readthedocs.io/en/latest/filtering.html#override-get-querylist)には載っています。
- 思ったよりも簡単に実装ができてしまいましたが以上で終了です。お付き合いいただきありがとうございました。