👌

DRFでUpdateAPIViewとListSerializerを利用して、Bulk Updateを実現する

2023/02/09に公開

書くこと

  • 一度のリクエストで複数のデータ更新ができるView, Serializerの定義
  • 更新時はBulk Updateするものとして、update queryは1件だけ発行
  • ListSerializerを継承した、汎用的なBulkUpdateListSerializerの定義
  • BulkUpdateListSerializerの活用法

利用する技術

  • Django Rest Framework
  • rest_framework.generics.UpdateAPIView
  • serializers.ModelSerializer
  • serializers.ListSerializer

想定シチュエーション

以下のモデルがある時、複数行のarticleレコードに対してtitleの更新を行う

models/article.py

class Article(models.Model):
    title = models.CharField(max_length=100)
    # (省略)

更新する値のdict配列。

data_list = [
   {
      "title": "更新後タイトル1"
   },
   {
      "title": "更新後タイトル2"
   },
]

方法論

ModelSerializer

serializers/bulk_update_article.py

from rest_framework.serializers import ModelSerializer
from .template.bulk_update_list_serializer import BulkUpdateListSerializer
from ..models import Article


class BulkArticleUpdateSerializer(ModelSerializer):
    class Meta:
        model = Article
        fields = ("title", "updated_at")
        list_serializer_class = BulkUpdateListSerializer

    def update(self, instance, validated_data):
        # Don't save() for bulk update
        instance.title = validated_data["title"]

        return instance

BulkUpdateListSerializer

serializers/template/bulk_update_list_serializer.py

from django.core.exceptions import ValidationError
from django.db import IntegrityError
from django.utils import timezone
from rest_framework.serializers import ListSerializer


class BulkUpdateListSerializer(ListSerializer):
    """
    Bulk Updateを実現するListSerializer
    """

    # auto_now optionをつけたカラム
    AUTO_NOW_FIELD = "updated_at"

    def update(self, instances, validated_data):
        bulk_instances = self.__bulk_instances(instances, validated_data)
        writable_fields = self.__writable_fields()

        # bulk updateではauto_nowフィールドが更新されない
        if self.__is_update_auto_now_field():
            self.__add_auto_now(bulk_instances)

        try:
            self.child.Meta.model.objects.bulk_update(bulk_instances, writable_fields)
        except IntegrityError as e:
            raise ValidationError(e)

        return bulk_instances

    def __bulk_instances(self, instances, validated_data):
        return [self.child.update(instance, data) for instance, data in zip(instances, validated_data)]

    def __is_update_auto_now_field(self):
        return self.AUTO_NOW_FIELD in self.child.Meta.fields

    def __writable_fields(self):
        fields = self.child.Meta.fields
        read_only_fields = self.__read_only_fields()

        return [n for n in fields if n not in read_only_fields]

    def __read_only_fields(self):
        try:
            return self.child.Meta.read_only_fields
        except AttributeError:
            # ModelSerializerでread_only_fieldsの定義を必須とすれば、try-except不要
            return ()

    def __add_auto_now(self, result):
        updated_at = timezone.now()
        for instance in result:
            instance.updated_date = updated_at

UpdateAPIView

views/multiple_article_update.py

from rest_framework.generics import UpdateAPIView
from ..models import Article
from ..serializers.bulk_update_article import BulkArticleUpdateSerializer


class MultipleArticleUpdate(UpdateAPIView):
    queryset = Article.objects.all()
    serializer_class = BulkArticleUpdateSerializer

    def get_object(self):
        # example: 
        # <ArticleQueryset [<Article: Article object (1)>, <Article: Article object (2)>]>
        ids = self.request.data.getlist("article_ids")
        return self.queryset.filter(pk__in=ids)

    def update(self, request, *args, **kwargs):
        # 更新対象のオブジェクト
        articles = self.get_object()

        # 更新後の値
        data = self.__get_data()

        serializer = self.get_serializer(instance=articles, data=data, many=True)
        self.perform_update(serializer)

        # (略)

    def perform_update(self, serializer):
        serializer.is_valid(raise_exception=True)
        serializer.save()

    def __get_data(self):
        # example:
        # data = [{'title': '更新タイトル1'}, {'title': '更新タイトル2'}]
        return data

BulkUpdateListSerializerの活用法

例の通り使うだけで問題ないですが、いくつかコメントを。

ModelSerializer

  1. list_serializerの設定
    2. list_serializer_class = BulkUpdateListSerializer
  2. updateのオーバーライド:
    3. オーバーライドしたメソッド内でinstance.save()を書かない
    4. 返り値は instance
class BulkArticleUpdateSerializer(ModelSerializer):
    class Meta:
        # 中略
        list_serializer_class = BulkUpdateListSerializer

    def update(self, instance, validated_data):
        # Don't save() for bulk update
        # 略
        return instance

UpdateAPIView

  1. serializer_classの設定:
    2. serializer_class = BulkArticleUpdateSerializer
    2. ModelSerializerをセット。ListSerializerの方ではない
  2. serializerの設定:
    3. serializer = self.get_serializer(instance=articles, data=data, many=True)
    4. instance=に、更新したいQueryset
    5. data=に、更新したいカラムと値を持ったDictの配列
    6. many=に、true option

class MultipleArticleUpdate(UpdateAPIView):
    # 略
    serializer_class = BulkArticleUpdateSerializer

    # 略

    def update(self, request, *args, **kwargs):
        # 略
        serializer = self.get_serializer(instance=articles, data=data, many=True)
        # 略

付録

Bulk Updateせずに1行づつUpdateするListSerializerが欲しい場合

UpdateListSerializer

class UpdateListSerializer(ListSerializer):
    """
    複数行のUpdateを実現するListSerializer
    """

    def update(self, instances, validated_data):
        response = []

        for instance, data in zip(instances, validated_data):
            # ここで1行づつupdate queryが発行
            response.append(self.child.update(instance, data))
        return response

ModelSerializer

class ArticleUpdateSerializer(ModelSerializer):
    class Meta:
        model = Article
        fields = ("title", "updated_at")
        list_serializer_class = UpdateListSerializer

    # updateはオーバーライドしない

UpdateAPIView

変更なし。省略。

参考文献

Discussion