0
0

More than 1 year has passed since last update.

ListSerializer を使ってImageFieldをbulk createする

Posted at

ゴール

ImageFieldを持つModelをRest API経由でcreateとbulk_createの2つを実現したい。

実装概要

  1. 公式のドキュメントにあるように、ListSerializer をカスタムして create メソッドからModelのbulk_createを呼ぶようにする
  2. Base64デコードするFieldクラスを作成する
  3. ModelSerializerlist_serializer_class に カスタムしたSerializer クラスを設定する
  4. ModelSerializer に Base64デコードするFieldを追加する
  5. APIViewget_serializer で POSTされたデータに基づき、many=True を設定する

前提

次のような、ユーザーが一枚一枚画像をアップロードできるフォームが作ってあるとする。

image.png

このForm/Viewは維持したまま、以下のようなREST APIも提供したい。
bulk アップロードのデータ構造がよくないが、とりあえずこれにする。

一枚アップロード
{
  "name": "sample image 001",
  "description": "This is sample image for uploading single image",
  "image": "<Base64エンコードされた画像データ>"
}
Bulkアップロード
[
  {
    "name": "sample image 001",
    "description": "This is the 1st sample image for bulk uploading multiple images",
    "image": "<Base64エンコードされた画像データ>"
  },
  {
    "name": "sample image 002",
    "description": "This is the 2nd sample image for bulk uploading multiple images",
    "image": "<Base64エンコードされた画像データ>"
  },
]

Django の Model、Form、View

妥当性やら脆弱性はこの際無視して、画像を保存できるようなModelと、ユーザーが一枚一枚画像をアップロードできるForm、Viewを定義する。

models.py
class Icon(models.Model):
    description= models.TextField(
        null=True,
        blank=True,
    )
    # API では image にしているが名前が違う
    image_path = models.ImageField(
        storage=select_storage,
        default='blank.svg',
        blank=False
    )
    name = models.CharField(
        max_length=ICON_NAME_MAX_LENGTH,
        validators=[
            MinLengthValidator(ICON_NAME_MIN_LENGTH)
        ],
        null=False,
        blank=False,
    )
forms.py
class IconUploadForm(ModelForm):
    class Meta:
        model = Icon
        fields = [ 'image', 'name', ]
views.py
class IconUploadView(CreateView):
    template_name = 'upload.html'
    form_class = IconUploadForm
    success_url = reverse_lazy('index')

Django REST frameworkでの実装方法

Serializerの実装

1. Base64をデコードするFieldクラスを定義する

ここらへん を参考に、serializer.ImageField.to_interval_value をオーバーライドして、デコード、ファイルデータ化、親クラスとしてのImageField への格納を行うクラスを作成する

serializer.py
class Base64ImageField(serializers.ImageField):
    def to_internal_value(self, data):
        if isinstance(data, str) and data.startswith('data:image'):
            format, imgstr = data.split(';base64,')
            ext = format.split('/')[-1]
            name = f"{uuid.uuid4().urn[9:]}.{ext}"
            data = ContentFile(name=name, content=base64.b64decode(imgstr))

        return super().to_internal_value(data)

2. カスタムなListSerializer クラスを定義する

公式のドキュメントにあるように、ListSerializer をカスタムして create メソッドからModelのbulk_createを呼ぶクラスを定義する。

serializer.py
class IconListSerializer(serializers.ListSerializer):

    def create(self, validated_data):
        icon_data_list = [Icon(**icon) for icon in validated_data]
        return Icon.objects.bulk_create(icon_data_list)

objects.bulk_create を使用しない場合、この定義は不要。
ドキュメントに記載されている通り、既定ではSerializerserializers.ListSerilizer を呼び出す。

3. ModelSerializerを作成し、カスタムしたクラスを設定する

  • Iconモデルの参照先名前を指定
    Base64ImageField をModelSerializserのフィールドとして設定する。このとき、models.py では image_path になっているので、source パラメーターでIconモデルの参照先名前を指定する。

    Django REST frameworkのドキュメント Serializer fields

    Metafields には Base64ImageField の 変数名を指定しておく

    image = Base64ImageField(source='image_path')
    
  • カスタムしたListSerializerを設定

  SerializerListSerializer の切り替えは、インスタンス化に際して many=True が与えられるかどうかで切り替える実装となっている。切り替えの実装はBaseSerializerクラスのコンストラクタを参照。

Django REST frameworkのドキュメント Customizing ListSerializer behavior

list_serializer_class = IconListSerializer

最終的なSerializerクラスは以下のような定義となる。

serializer.py
class IconSerializer(serializers.ModelSerializer):
    image = Base64ImageField(source='image_path')

    class Meta:
        model = Icon
        list_serializer_class = IconListSerializer
        fields = [
            'name', 'description', 'image'
        ]

Viewの実装

serializer_classget_serializer メソッドでインスタンス化される。

generics.APIViewの実装を見ると、get_serializer に与える *args**kwargs は、そのまま serializer_classのコンストラクタに引き渡される。結果、get_serializerメソッドにmany=Trueを引数として与えれば、serializer_classlist_serializer_class がインスタンスにとして返却される。

つまり、受信データがbulkアップロードのデータ構造となっているかどうかを判定して、親クラスのget_serializer の引数に many=True を設定する・しないを切り替えるようにする。

views.py
class IconBulkUploadView(generics.CreateAPIView):
    serializer_class = IconSerializer

    def get_serializer(self, *args, **kwargs):
        # ここで受信データがリスト形式か、オブジェクト形式かを判定する
        # リスト形式なら many=True を設定する
        if isinstance(kwargs.get('data', {}), list):
            kwargs['many'] = True

        # many=True`を引数として与えれば、`list_serializer_class` のインスタンスが返却される
        return super().get_serializer(*args, **kwargs)

残りのもろもろ

urls.py では views.pygenerics の view であるから、 router は使用せずに、そのままurlpatternsに設定する。

urls.py
urlpatterns = [
    # いろいろ ----
    path('bulk', views.IconBulkUploadView.as_view(), name='bulk_upload'),
] 
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0