Skip to content

PrimaryKeyRelatedField는 모델 필드를 덮어씌우며, 연관 모델의 인스턴스를 저장하지 않습니다 {drf}

https://github.com/NohSungwoo/Plana/pull/25/files#diff-f96949d693834953aca5d343f59b82cb607f979dd16891020a6225a084b2b9e8R13 해당 PR의 memos/serializers.py 파일에서 의문이 생겼습니다.

Serializer > Meta > fields 에 포함된 필드는 Model 필드를 덮어씌우는 것이 아닐까? 아래의 Serializer는 memo_schedule 이라는 모델 필드 대신에 단순 int 타입 PrimaryKeyRelatedField으로 치환할 뿐인가? 아니면 내부적으로 Schedule 이라는 연관 모델의 인스턴스를 들고 있을까?

class MemoDetailSerializer(s.ModelSerializer):
    memo_set = s.PrimaryKeyRelatedField(queryset=MemoSet.objects.all())
    memo_schedule = s.PrimaryKeyRelatedField(read_only=True)
    memo_todo = s.PrimaryKeyRelatedField(read_only=True)

    class Meta:
        model = Memo
        fields = (
            "id",
            "title",
            "memo_set",
            "text",
            "memo_schedule",
            "memo_todo",
        )

IF 덮어씌우는 것이 맞다:#

Serializer는 한 번 생성된 이후로는 serializer.instance의 연관 인스턴스를 저장하지 않는다는 뜻이다. 이 경우 serializer.instance.memo_schedule.joined_column 이런 사용이 불가능해진다는 뜻이기도 하다.

ELSE 덮어씌우지 않고 모델 인스턴스를 가지고 있다:#

Serializer가 인스턴스와 연관 인스턴스들을 전부 가지고 있는 경우, 적어도 update, create 같은 메서드를 오버라이드 할때 편해질 것이다.

Source Code#

https://github.com/encode/django-rest-framework/blob/3.15.2/rest_framework/relations.py#L249-L250

class RelatedField(Field)
        def use_pk_only_optimization(self):
                return False

    def get_attribute(self, instance):
        if self.use_pk_only_optimization() and self.source_attrs:
            # Optimized case, return a mock object only containing the pk attribute.
            with contextlib.suppress(AttributeError):
                attribute_instance = get_attribute(instance, self.source_attrs[:-1])
                value = attribute_instance.serializable_value(self.source_attrs[-1])
                if is_simple_callable(value):
                    # Handle edge case where the relationship `source` argument
                    # points to a `get_relationship()` method on the model.
                    value = value()

                # Handle edge case where relationship `source` argument points
                # to an instance instead of a pk (e.g., a `@property`).
                value = getattr(value, 'pk', value)

                return PKOnlyObject(pk=value)
        # Standard case, return the object instance.
        return super().get_attribute(instance)

PrimaryKeyRelatedField#

class PrimaryKeyRelatedField(RelatedField):
    def use_pk_only_optimization(self):
        return True

PrimaryKeyRelatedFielduse_pk_only_optimization()이 True이다. 즉, RelatedFieldget_attribute 메서드에서 오직 .pk 속성만 가지고 있는 목 객체를 리턴한다고 되어있다. 따라서 연관 필드가 PrimaryKeyRelatedField 일 경우, 연관인스턴스는 가지고 있지 않는 것이라고 볼 수 있다.

HyperlinkedRelatedField#

class HyperlinkedRelatedField(RelatedField):
    def use_pk_only_optimization(self):
        return self.lookup_field == 'pk'

    def get_object(self, view_name, view_args, view_kwargs):
        """
        Return the object corresponding to a matched URL.

        Takes the matched URL conf arguments, and should return an
        object instance, or raise an `ObjectDoesNotExist` exception.
        """
        lookup_value = view_kwargs[self.lookup_url_kwarg]
        lookup_kwargs = {self.lookup_field: lookup_value}
        queryset = self.get_queryset()

        try:
            return queryset.get(**lookup_kwargs)
        except ValueError:
            exc = ObjectValueError(str(sys.exc_info()[1]))
            raise exc.with_traceback(sys.exc_info()[2])
        except TypeError:
            exc = ObjectTypeError(str(sys.exc_info()[1]))
            raise exc.with_traceback(sys.exc_info()[2])

HyperlinkedRelatedField는 기본적으로 use_pk_optimization()이 True입니다, 굳이 .lookup_field를 변경하지 않는 이상. 그리고 최적화가 되어있기 때문에 연관 인스턴스에 대한 내용이 없어 get_object 메서드를 통하여 DB에 쿼리를 한 번 더 날려야 하는 것으로 인다. 이 부분은 좀 주의해서 다뤄야 할 것 같다. 별 생각없이 연관 인스턴스 가지고 오다간 수많은 쿼리의 향연을 만나게 될 것이다.

HyperlinkedIdentityField#

class HyperlinkedIdentityField(HyperlinkedRelatedField):
    def use_pk_only_optimization(self):
        # We have the complete object instance already. We don't need
        # to run the 'only get the pk for this relationship' code.
        return False

반면에 HyperlinkedIdentityFielduse_pk_only_optimization() 이 True이다. 그리고 그 주석을 읽어보면…

우리는 이미 온전한 객체 인스턴스를 이미 가지고 있습니다. 굳이 “이 관계를 위한 유일한 pk”를 다룰 필요가 없습니다.

이는 목 객체가 필요 없다는 뜻이지, 기본적으로는 HyperlinkedRelatedField를 상속받고 있기 때문에 연관 인스턴스의 속성을 조회하기 위해선 get_object를 통해야 할 것이다.


결론: PrimaryKeyRelatedField는 모델 필드를 덮어씌우며, 연관 모델의 인스턴스를 저장하지 않습니다.#

PrimaryKeyRelatedFielduse_pk_only_optimization() 메서드가 True로 설정되어 있습니다. 이로 인해 DRF의 get_attribute 메서드는 최적화된 방식으로 작동하며, 연관된 모델 인스턴스를 참조하지 않고 PKOnlyObject를 반환합니다. 따라서, 이 필드를 Serializer에서 사용할 경우 다음과 같은 특징이 있습니다:

  1. 연관 인스턴스 비저장:
    • memo_schedule 또는 memo_todo와 같은 PrimaryKeyRelatedField는 단순히 PK 값을 반환합니다.
    • 결과적으로 serializer.instance.memo_schedule로 연관 모델의 속성(예: joined_column)에 접근할 수 없습니다.
    • 연관 필드 값이 필요할 경우 추가적으로 쿼리를 실행해야 합니다.
  2. 모델 필드 덮어쓰기:
    • Serializer의 fields에 포함된 필드는 모델의 기본 동작을 덮어씌웁니다.
    • PrimaryKeyRelatedField는 기본적으로 연관 필드를 단순한 정수형 PK로 표현합니다.
  3. Hyperlinked 필드와의 비교:
    • HyperlinkedRelatedField 또는 HyperlinkedIdentityFielduse_pk_only_optimization()의 설정에 따라 연관 인스턴스를 사용할 수도 있습니다. 그러나 기본 설정에서는 URL만 반환하며, 실제 객체를 가져오려면 추가적인 DB 조회가 필요합니다.

실무적 고려사항:#

  1. 필요한 필드 설계:
    • 연관 필드가 단순히 참조(PK)만 필요한 경우 PrimaryKeyRelatedField를 사용해 효율적으로 처리할 수 있습니다.
    • 반면, 연관 모델의 속성에 접근해야 한다면 별도의 SerializerMethodField를 정의하거나, 모델 필드를 덮어쓰지 않도록 설정해야 합니다.
  2. 최적화 고려:
    • PrimaryKeyRelatedField를 사용할 때는 불필요한 DB 조회를 피할 수 있지만, 연관 데이터가 필요할 경우 추가 쿼리를 발생시키므로 주의해야 합니다.
    • N+1 문제를 방지하려면 DRF의 select_relatedprefetch_related를 적절히 활용하는 것이 중요합니다.
  3. 명확한 의사결정:
    • 특정 필드가 연관 모델의 속성이나 데이터를 포함해야 한다면 Serializer에서 명시적으로 정의하고 필요한 데이터를 포함하는 것이 좋습니다. 예를 들어, memo_schedule 필드에 대한 상세 정보를 제공하려면 Nested Serializer를 사용하는 방법도 고려할 수 있습니다.

위 결론을 바탕으로 PR의 Serializer가 연관 인스턴스를 저장하지 않는다는 점을 염두에 두고, 연관 모델의 속성이 필요한 경우 대체 설계를 고려하세요.