본문 바로가기

Django and Mongo

Django REST API & Djongo - 5. CustomSerializer

본 작성자는 아직 1년차도 못채운 햇병아리 코더입니다.

 

(사내에 장고를 다루는 사람이 저 혼자라서) 기술한 내용중에 틀린 내용이 있을 수도 있습니다.

 

예제랍시고 올려놓은 코드가 아주 허접할 수도 있습니다.

 

미숙한 제게 조언을 주실 수 있다면 댓글 부탁드립니다. 겸허히 배우겠습니다.

 

개발 대상 사양은 모두 아래를 공통적으로 사용합니다.

[개발 OS : Windows10]

[설치 OS : CentOS 8 with gnomeUI]

[Python: 3.8]

[Mongodb : 4.4.1]

[PostgreSQL : 12.4]

[Elasticsearch : 7.9.3]

[Tensorflow : 2.4.1]

[scikit-learn : 0.23.2]

 

<수정이력>

: 2021-04-13 => 코드 위 파일명 어노테이션 누락분 수정.

------------------------------------------------------------------

 

지난시간에 Custom Field와 Embeded Field로 이미지 업로드와 자동 텍스트 추출을 구현했습니다.

 

이번에는 이어서 다른 방법으로 같은 내용을 구현해볼까합니다.

마찬가지로 이미지를 업로드 받으면, 이 이미지를 "알아서 잘" 요리해서 추출한 텍스트와 함께 집어 넣어야 합니다.

 

지난 포스트의 POST요청 결과인데, null이 너무나 거슬린다. Serializer를 커스텀 하면서 저것도 지울 수 있도록 해보자.

 

자바 환경에서 코딩을 하다가 파이썬을 다루면 가끔 문서가 너무 불친절한게 아닌가 싶을때가 있습니다. 특히나 시간에 쫓기고 있을때는 더더욱 그렇죠.

 

그래서 문서가 부족할때, 꼭 클래스 정의에 가보면 주석으로 문서를 만들어놓는 경우가 있어 확인해보면 큰 도움이 됩니다. (자바라고 안그런건 아니지만 문서보다 더 빨리 알려줄 선배들이 있다.)

 

 

class ModelSerializer(Serializer):
    """
    A `ModelSerializer` is just a regular `Serializer`, except that:

    * A set of default fields are automatically populated.
    * A set of default validators are automatically populated.
    * Default `.create()` and `.update()` implementations are provided.

    The process of automatically determining a set of serializer fields
    based on the model fields is reasonably complex, but you almost certainly
    don't need to dig into the implementation.

    If the `ModelSerializer` class *doesn't* generate the set of fields that
    you need you should either declare the extra/differing fields explicitly on
    the serializer class, or simply use a `Serializer` class.
    """
    serializer_field_mapping = {
        models.AutoField: IntegerField,
        models.BigIntegerField: IntegerField,
        models.BooleanField: BooleanField,
        models.CharField: CharField,
        models.CommaSeparatedIntegerField: CharField,
        models.DateField: DateField,
        models.DateTimeField: DateTimeField,
        models.DecimalField: DecimalField,
        models.DurationField: DurationField,
        models.EmailField: EmailField,
        models.Field: ModelField,
        models.FileField: FileField,
        models.FloatField: FloatField,
        models.ImageField: ImageField,
        models.IntegerField: IntegerField,
        models.NullBooleanField: BooleanField,
        models.PositiveIntegerField: IntegerField,
        models.PositiveSmallIntegerField: IntegerField,
        models.SlugField: SlugField,
        models.SmallIntegerField: IntegerField,
        models.TextField: CharField,
        models.TimeField: TimeField,
        models.URLField: URLField,
        models.UUIDField: UUIDField,
        models.GenericIPAddressField: IPAddressField,
        models.FilePathField: FilePathField,
    }
    if hasattr(models, 'JSONField'):
        serializer_field_mapping[models.JSONField] = JSONField
    if postgres_fields:
        serializer_field_mapping[postgres_fields.HStoreField] = HStoreField
        serializer_field_mapping[postgres_fields.ArrayField] = ListField
        serializer_field_mapping[postgres_fields.JSONField] = JSONField
    serializer_related_field = PrimaryKeyRelatedField
    serializer_related_to_field = SlugRelatedField
    serializer_url_field = HyperlinkedIdentityField
    serializer_choice_field = ChoiceField

    # The field name for hyperlinked identity fields. Defaults to 'url'.
    # You can modify this using the API setting.
    #
    # Note that if you instead need modify this on a per-serializer basis,
    # you'll also need to ensure you update the `create` method on any generic
    # views, to correctly handle the 'Location' response header for
    # "HTTP 201 Created" responses.
    url_field_name = None

    # Default `create` and `update` behavior...
    def create(self, validated_data):
        """
        We have a bit of extra checking around this in order to provide
        descriptive messages when something goes wrong, but this method is
        essentially just:

            return ExampleModel.objects.create(**validated_data)

        If there are many to many fields present on the instance then they
        cannot be set until the model is instantiated, in which case the
        implementation is like so:

            example_relationship = validated_data.pop('example_relationship')
            instance = ExampleModel.objects.create(**validated_data)
            instance.example_relationship = example_relationship
            return instance

        The default implementation also does not handle nested relationships.
        If you want to support writable nested relationships you'll need
        to write an explicit `.create()` method.
        """
        raise_errors_on_nested_writes('create', self, validated_data)

        ModelClass = self.Meta.model

        # Remove many-to-many relationships from validated_data.
        # They are not valid arguments to the default `.create()` method,
        # as they require that the instance has already been saved.
        info = model_meta.get_field_info(ModelClass)
        many_to_many = {}
        for field_name, relation_info in info.relations.items():
            if relation_info.to_many and (field_name in validated_data):
                many_to_many[field_name] = validated_data.pop(field_name)

        try:
            instance = ModelClass._default_manager.create(**validated_data)
        except TypeError:
            tb = traceback.format_exc()
            msg = (
                'Got a `TypeError` when calling `%s.%s.create()`. '
                'This may be because you have a writable field on the '
                'serializer class that is not a valid argument to '
                '`%s.%s.create()`. You may need to make the field '
                'read-only, or override the %s.create() method to handle '
                'this correctly.\nOriginal exception was:\n %s' %
                (
                    ModelClass.__name__,
                    ModelClass._default_manager.name,
                    ModelClass.__name__,
                    ModelClass._default_manager.name,
                    self.__class__.__name__,
                    tb
                )
            )
            raise TypeError(msg)

        # Save many-to-many relationships after the instance is created.
        if many_to_many:
            for field_name, value in many_to_many.items():
                field = getattr(instance, field_name)
                field.set(value)

        return instance

    def update(self, instance, validated_data):
        raise_errors_on_nested_writes('update', self, validated_data)
        info = model_meta.get_field_info(instance)

        # Simply set each attribute on the instance, and then save it.
        # Note that unlike `.create()` we don't need to treat many-to-many
        # relationships as being a special case. During updates we already
        # have an instance pk for the relationships to be associated with.
        m2m_fields = []
        for attr, value in validated_data.items():
            if attr in info.relations and info.relations[attr].to_many:
                m2m_fields.append((attr, value))
            else:
                setattr(instance, attr, value)

        instance.save()

        # Note that many-to-many fields are set after updating instance.
        # Setting m2m fields triggers signals which could potentially change
        # updated instance and we do not want it to collide with .update()
        for attr, value in m2m_fields:
            field = getattr(instance, attr)
            field.set(value)

        return instance

    # Determine the fields to apply...

어셈블러까지 들어갈게 아니라면 딱 여기서 멈추고, 동작을 핵심을 살펴보겠습니다.

 

매개변수 이름이 validated_data 라는 점에서 어느 정도 내부 처리 순서가 보이는 군요.

class CreateModelMixin:
    """
    Create a model instance.
    """
    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        headers = self.get_success_headers(serializer.data)
        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

    def perform_create(self, serializer):
        serializer.save()

    def get_success_headers(self, data):
        try:
            return {'Location': str(data[api_settings.URL_FIELD_NAME])}
        except (TypeError, KeyError):
            return {}

 

아니나 다를까, ModelViewSet이 상속받는 CreateModelMixin에서 그 단편이 보입니다.

View에서 Serializer의 create를 호출하는 구조로 보이네요. 그리고 그 전에 validation을 진행하는군요

 

serializer.save()는 수많은 예외처리와 함께 serializer의 update와 create를 분기해주는 역할을 합니다.

만약 serializer를 커스텀해서 사용하려면 간단하게 이 2개의 메서드를 오버라이딩 해주면 되겠군요.

 

데이터 모델은 중간과정을 생략하고 최종본만 간략하게 만들면 될 것 같습니다.

 

 

<ocrapp/models.py>

class UserImages(models.Model):
    _id = models.ObjectIdField()
    image_title = models.TextField(null=False, default="a.jpg")
    image = models.ImageField(null=False, upload_to="uploaded_pictures")
    image_context = models.TextField(null=False, default="일방통행")

    class Meta:
        db_table = "user_images"

이번의 context 에는 간단하게 OCR로 추출한 텍스트 정보를 뭉뚱그려 집어넣겠습니다.

OCR 추출 코드도 다시 바뀌여야 겠지요.

 

<ocrapp/ocr_method/py_tesseract_ocr.py>

def tesseract_ocr_extract_from_file_only_text(image_file):
    image = cv2.imdecode(np.frombuffer(image_file.read(), np.uint8), cv2.COLOR_BGR2RGB)
    rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
    results = pytesseract.image_to_data(rgb, output_type=pytesseract.Output.DICT)
    return " ".join([text for text in results["text"] if text])

기타 모든 정보를 무시하고 텍스트만 하나의 문자열로 합치기로 합니다.

 

자 이제 시리얼 라이저를 커스텀 할 차례입니다.

validated_data 는 단순한 dict형의 객체입니다. 여기에는 모델에 선언된 필드의 이름을 키값으로 가지는 요소가 포함되어야 합니다.

 

이때의 validated_data에 원하는 값을 넣어주면 Request에는 없는 필드도 만들어서 넣어줄 수 있게 됩니다.

 

<ocrapp/serializer.py>

class UserImageSerializer(serializers.ModelSerializer):
    class Meta:
        model = UserImages
        fields = "__all__"

    def create(self, validated_data):
        validated_data["image_context"] = tesseract_ocr_extract_from_file_only_text(validated_data["image"])
        validated_data["image_title"] = validated_data["image"].name
        return super(UserImageSerializer, self).create(validated_data)

    def update(self, instance, validated_data):
        validated_data["image_context"] = tesseract_ocr_extract_from_file_only_text(validated_data["image"])
        validated_data["image_title"] = validated_data["image"].name
        return super(UserImageSerializer, self).update(instance, validated_data)

 

그런데 문제가 하나 있습니다. 큰 문제는 아니고, ImageField의 특징 때문에 발생하는 문제인데, 이 이미지 필드는

url을 돌려줍니다. 받을때는 이미지 파일을 받습니다. 직관적이지 않아서 네이밍이 조금 그렇네요.

 

 

요청시 image 파라미터에는 파일이 들어가고,

응답시 image 파라미터에는 url이 돌아오게 됩니다.

 

전혀 직관적이지 않아서 좀 문제가 있네요.

 

다음시간에는 view의 create 메서드와 validation 을 수정해서 더 개선을 해보도록 하겠습니다.