Django REST framework で一括登録のエンドポイントを作る（bulk_create）

# Django REST frameworkのレコードの一括作成
Django REST framework（以下DRF）のクラスベースViewには標準でレコードを一括で登録する機能は含まれていない。


複数のデータを登録したいとき以下の3つの方法が考えられる。

1. 繰り返しPOSTを送る
1. リストで送ってORMで繰り返し登録するエンドポイントを作る
1. リストで送って一括で登録するエンドポイントを作る

> 1.繰り返しPOSTを送る

この方法は通信回数が多くなってしまう。


> 2.リストで送ってORMで繰り返し登録するエンドポイントを作る

当初この方法で実現しようとしたが、同時登録数が多いと時間がかかり、フロントへのレスポンスが遅くなってしまった。
さらに、本番環境ではRDBへの繰り返し処理は料金に直結してしまう。

> 3.リストで送って一括で登録するエンドポイントを作る

調べていくと一括登録する`bulk_create()`というメソッドがあることを知った。
https://docs.djangoproject.com/en/4.0/ref/models/querysets/#bulk-create

しかし、DRFで使う方法があまりまとめられていなくて、結構ハマってしまったので、使い方を共有します！

## 前提ファイル

### モデル

今回は例として本モデルで作っていく。

~~~python:models.py
class Book(models.Model):
    id = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False, unique=True)
    title = models.CharField(verbose_name='題名')
    author = models.CharField(verbose_name='著者')
    price = models.IntegerField(verbose_name='価格')
~~~


### シリアライザ

~~~python:serializers.py
class BookSerializer(serializers.ModelSerializer): 
    class Meta:
        model = Book
        fields = __all__
~~~

### ビュー

~~~python:views.py
class BookViewSet(viewsets.ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookSerializer
~~~

### URL

~~~python:urls.py
router.register('book', BookViewSet)
~~~
---
この状態から本レコードを一括登録するエンドポイントを作っていきます！

## リストシリアライザの追加
シリアライザの作成で継承するクラスは `rest_framework.serializers.Serializer`と`ListSerializer` の２種類がある。 
単一のリソースを扱う場合は前者を、 複数のリソースを扱う場合は後者を継承する。
今回は、複数のデータを登録したいので、`ListSerializer`を継承するクラスを作る。

~~~python:serializers.py
class CreateBookListSerializer(serializers.ListSerializer):
    def create(self, validated_data):
        result = [Book(**attrs) for attrs in validated_data]
        Book.objects.bulk_create(result)
        return result
~~~
少し意外なのは、このシリアライザの時点で`bulk_update`を使うということだ。

これを元々のシリアライザで使えるようにするために、１行追記する。

~~~python:serializers.py
class BookSerializer(serializers.ModelSerializer): 
    class Meta:
        model = Book
        fields = __all__
        list_serializer_class = CreateBookListSerializer # 追記
~~~

## ビューの追加
上で作成したシリアライザを使って、複数登録を行うエンドポイントを作成していく。
今回は、createだけできればいいので、汎用APIViewである`CreateAPIView`を継承して作成する。

~~~python:views.py
class BulkCreateBookView(generics.CreateAPIView):
    serializer_class = BookSerializer

    def get_serializer(self, *args, **kwargs):
        if isinstance(kwargs.get("data", {}), list):
            kwargs["many"] = True
        return super(BulkCreateBookView, self).get_serializer(*args, **kwargs)
~~~

単一登録も複数登録も同じシリアライザ(`BookSerializer`)を通すので、リクエストのbodyが単一オブジェクトなら従来のもの、リスト形式であればリストシリアライザを使えるように、`get_serializer`をオーバーライドしている。

## URLの登録
もうここまで来ればできたも同然。
複数登録するためのURLを作成したら終わりです！
好きな名前で登録しましょう。

~~~python:urls.py
router.register('book', BookViewSet)
# 追記
urlpatterns = [
    path('book/bulk', BulkCreateBookView.as_view(), name='books'),
]
~~~
`viewset`を継承したときと`generics`を継承したときで書き方が違うので注意！
これで完成です😇

## 使えるか確認
DRFには標準でGUIからAPIを試せるコンソールがあるが、そこから登録してしまうと

~~~
TypeError at /api/book/bulk
'CreateBookListSerializer' object is not iterable
~~~
とエラーが出てしまう。
これはこのコンソールがレスポンスとして複数のオブジェクトを想定していないことが原因だと考えられる。
APIに問題があるわけではないので、POSTMAN等を使って確認してみるとうまくいくことがわかる。

テストは他のものと同様に書けるので、書きましょう！