본문 바로가기

Django and Mongo

Django REST API & Djongo - 2. ObjectID

본 작성자는 아직 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]

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

 

이전 챕터에서 아래와 같은 모델을 만들어서 사용했었습니다.

 

<ocrapp/models.py>

from djongo import models


class PreprocessedImages(models.Model):
    image_id = models.AutoField(primary_key=True)  # 나중에 수정할 부분입니다!
    image_title = models.TextField(null=True, default="a.jpg")
    image_path = models.FilePathField(null=False, default="/home/fffcoder/ocrandnlp/images/a.jpg")
    image_context = models.TextField(null=False, default="일방통행")

    class Meta:
        db_table = "preprocessed_images"

 

몽고DB는 기본적으로 모든 bson 파일에 '_id' field 를 만들고, 이를 키값으로 사용합니다.

그런데,  위와 같은 모델로 데이터를 하나 생성해보면, 자동으로 '_id' field가 생성되고 ObjectID가 됨을 알 수 있습니다.

 

위 구조대로라면 키값이 2개가 되는 비효율적인 구조가 되는 것입니다.

 

먼저 이 부분부터 처리하도록 하겠습니다.

 

 

<ocrapp/models.py>

from djongo import models


class PreprocessedImages(models.Model):
    _id = models.ObjectIdField()
    # image_id = models.AutoField(primary_key=True)
    image_title = models.TextField(null=True, default="a.jpg")
    image_path = models.FilePathField(null=False, default="/home/fffcoder/ocrandnlp/images/a.jpg")
    image_context = models.TextField(null=False, default="일방통행")

    class Meta:
        db_table = "preprocessed_images"

어차피 '_id' 가 자동으로 생성되므로 image_id 필드는 필요가 없습니다.

대신, ' 자동 생성될 '_id' 필드를 명시해줌으로써 장고에 이를 사용하겠다 알려야 합니다. primary_key 속성은 굳이 명시하지 않아도 됩니다.

 

아래는 djongo의 내장 코드입니다만 이미 primary_key가 선언되어 있습니다.

class ObjectIdField(_ObjectIdField):
    """
    For every document inserted into a collection MongoDB internally creates an field.
    The field can be referenced from within the Model as _id.
    """

    def __init__(self, *args, **kwargs):
        id_field_args = {
            'primary_key': True,
            'auto_created': True
        }
        id_field_args.update(kwargs)
        super().__init__(*args, **id_field_args)

 

이제 이 데이터 모델의 pk는 _id 입니다.

그런데 몽고DB의 ObjectID는 12byte의 이진 데이터이고, djongo의 기반이 되는 pymongo에서는 ObjectID라는 객체로 따로 관리하게 됩니다.

 

백문이 불여일견, 일단 데이터를 하나 만들어 봅시다.

http://localhost:8000/ocr/preprocessed_image/

위 url로 body가 비어있는 post 요청을 하나 날려보시면, 모델의 모든 필드에 기본값이 설정되어 있기 때문에 데이터 하나가 만들어 질 것입니다.

 

data grip에서 확인한 mongodb - "preprocessed_images" collection

이제 우리는 _id의 값이 "604327b5663f459082617097" 이라는 것을 알았습니다. 하지만 이 값은 엄밀히 따져 진짜 ObjectID 객체의 값을 나타내지는 않습니다.

 

GET 요청을 

http://localhost:8000/ocr/preprocessed_image/604327b5663f459082617097/

위 url로 날려보면 당연히 not found 로 리턴값이 돌아오게 됩니다.

 

postman을 사용한 예시.

 

ObjectID 객체는 따로 처리를 해줄 필요가 있습니다.

 

기본적으로 DRF(Django Rest Framework)의 router는 각각의 HTTP 메서드가 ModelViewSet의 메서드에 1대1로 매칭되어 있습니다.

 

<Detail View>
'get': 'retrieve',
'put': 'update',
'patch': 'partial_update',
'delete': 'destroy'

 

<List View>
'get': 'list',
'post': 'create'

 

만약 어떤식으로든 각 메서드의 처리를 달리해야할 필요가 있다면 해당 메서드를 오버라이딩하는 것으로 그 처리를 담당하게 할 수 있습니다.

 

 

이번 경우에는 retrieve 함수를 수정해야합니다. retieve 함수의 경우에는 RetrieveModelMixin 클래스에서 상속받은 메서드입니다. 내용은 아래와 같고 여기에 다양한 함수를 커스텀할 수 있습니다.

 

<mixin.py>

class RetrieveModelMixin:
    """
    Retrieve a model instance.
    """
    def retrieve(self, request, *args, **kwargs):
        instance = self.get_object()
        serializer = self.get_serializer(instance)
        return Response(serializer.data)

 

위 코드를 베껴서 실제의 코드에 적용해봅니다.

 

<ocrapp/views.py>

from rest_framework.viewsets import ModelViewSet
from rest_framework.response import Response
from ocrapp.serializers import PreprocessedImageSerializer
from ocrapp.models import PreprocessedImages
from bson import ObjectId


class PreprocessedImageViewSets(ModelViewSet):
    queryset = PreprocessedImages.objects.all()
    serializer_class = PreprocessedImageSerializer

    def retrieve(self, request, *args, **kwargs):
        self.kwargs["pk"] = ObjectId(self.kwargs["pk"])
        instance = self.get_object()
        serializer = self.get_serializer(instance)
        return Response(serializer.data)

 

이때, get_object() 함수에서는  pk를 self.kwargs에서 가져오므로 str로 되어있는 kwargs["pk"]를 ObjectId 로 바꾸어주면 됩니다.

(bson 모듈은 djongo를 설치하는 과정에서 자동 설치되어 있습니다.)

 

이제 테스트를 해보면 url에 ObjectId를 넘겨서 처리할 수 있습니다.

 

GET 방식을 처리했으니 같은 방식으로 다른 메서드들도 오버라이딩 해주면 됩니다.

 

다만 위 방식으로 처리하는 것은 비효율적이니 데코레이터를 하나 만들어서 처리합니다.

 

<ocrapp/decorators.py>

from bson import ObjectId


def request_converting_to_object_id(func):
    def object_id_insert(*args, **kwargs):
        args[0].kwargs["pk"] = ObjectId(kwargs["pk"])
        response = func(*args, **kwargs)
        return response
    return object_id_insert

 

<ocrapp/views.py>

from rest_framework.viewsets import ModelViewSet
from ocrapp.serializers import PreprocessedImageSerializer
from ocrapp.models import PreprocessedImages
from ocrapp.decorators import request_converting_to_object_id


class PreprocessedImageViewSets(ModelViewSet):
    queryset = PreprocessedImages.objects.all()
    serializer_class = PreprocessedImageSerializer

    @request_converting_to_object_id
    def retrieve(self, request, *args, **kwargs):
        return super().retrieve(self, request, args, kwargs)

    @request_converting_to_object_id
    def partial_update(self, request, *args, **kwargs):
        return super().partial_update(self, request, args, kwargs)

    @request_converting_to_object_id
    def update(self, request, *args, **kwargs):
        return super().update(self, request, args, kwargs)

    @request_converting_to_object_id
    def destroy(self, request, *args, **kwargs):
        return super().destroy(self, request, args, kwargs)

메서드는 더 있지만, pk를 활용하는 메서드는 이 4개이므로 일단 이것만 수정해주면, ObjectID를 하나의 pk로 사용할 수 있게 됩니다!

 

 

 

여기까지 진행된 시점에서 잠깐 serializer도 짚고 넘어가겠습니다.

분명 요청은 처리가 되어있는데 응답부분이 처리가 안되어 있지 않나요?

 

이전시간에서는 image_id를 read_only_field로 돌려두었습니다. 그리고 이걸 "_id" 로 변경한다 한들 아직 출력되지는 않지요.

 

<ocrapp/serializers.py>

from rest_framework import serializers
from ocrapp.models import PreprocessedImages


class PreprocessedImageSerializer(serializers.ModelSerializer):
    class Meta:
        model = PreprocessedImages
        # fields = '__all__'
        fields = ["image_title", "image_context", "_id"]
        # read_only_fields = ["image_id"]

 

 

일단, API가 ObjectID도 반환하도록 serializer를 바꾸어 보겠습니다.

이때, djongo와 DRF의 시너지가 여기서 발휘됩니다.

 

postman을 활용한 예시.

 

반환받은 "_id" 는 단순한 스트링이기 때문에 이렇게 내뱉은 key값을 나중에 다시 받을 수도 있게 됩니다!

 

이제 이 API는 몽고DB의 기본 제공 id값인 ObjectId로 CRUD 작업을 할 수 있게 됩니다.

 

 

참고

https://velog.io/@jcinsh/RetrieveUpdateDestroyView-%EC%9D%B4%ED%95%B4