들어가기에 앞서…


안녕하세요, 저는 베이비 개발자👶이자 이제 5달차가 되어가는 백엔드 개발자입니다. (요즘 밀고 있어요 응애)

저번 블로그 글에 이어 이렇게 또 다시 Django의 쿼리 관련 글을 쓰게 되었습니다.

저번 블로그 글과 연관성이 있기 때문에 저번 글도 읽고 오신다면 좋습니다.😉

[저번블로그글]

쿼리 함수는 돌아오는 거야


쿼리의 무서움을 느껴서 일까요? 쿼리 최적화를 위해 쿼리의 갯수 하나하나에 집착하게 되었습니다.

저: 쿼리가 하나… 줄었다… 으히히🥴

이럴 정도로 쿼리에 집착하며 코드를 짜고 있는데요(실제로는 이렇지 않습니다)(진짜로정말로)

그렇다보니 select_related와 prefetch_related, 그리고 Prefetch는 거의 필수적으로 사용하고 있죠.

하지만 이것만으로도 해결이 안되는 경우가 종종 있고는 합니다. 특히나 새로운 필드를 추가해서 보내줘야 할 경우는 난감할 때가 있고는 합니다.

class Profile(models.Model):
    user = models.ForeignKey('User', on_delete=models.CASECADE)
    name = models.CharField(max_length=128)

class Subject(models.Model):
    user = models.ForeignKey('User', on_delete=models.CASECADE)
    title = models.CharField(max_length=128)

가령 profile을 보내줄 때 같은 user가 작성한 subject의 title들도 같이 보내주려 할 경우,

class ProfileSerializer(serializers.ModelSerializer):
    subject_title = serializers.SerializerMethodField()

    def get_subject_title(self, obj):
        user = obj.user
        return user.subject_set.all().values_list('title', flat=True)

    class Meta:
        model = Profile
        fields = ['name', 'subject_title']

기존에는 serializer의 SerializerMethodField를 이용해 값을 넘겨주고는 했습니다. 하지만 이렇게 코드를 작성할 경우, user를 호출할 뿐만 아니라 또 다시 subject 값을 가져오기 위해 DB에 접근해야하니 쿼리가 여러 번 발생하게 됩니다.

p = Profile.objects.prefetch_related(Prefetch('user', queryset=User.objects.prefetch_related('subject_set').all())).get(id=1)

혹은 Prefetch를 이용해 캐싱하는 방법도 있습니다.

하지만 저는 무언가 다른 방법을 찾을 수 없을까 고민하던 찰나, annotate를 사용하는 방법을 떠올리게 되었습니다.

사실 annotate같은 경우, 얼마 전까지만 해도 어렵다보니 사용하지 않기 위해 바락바락 애를 썼었어요. 하지만 결국 돌고돌아 annotate를 사용하게 되더군요😥 마치 부메랑처럼…

쿼리는 돌아오는 거야
쿼리는 돌아오는 거야

저는 결국 저에게 돌고 돌아온 이 annotate를 써야만 했습니다. 다른 방법이 없는 것은 아니었지만, annotate를 계속해서 피할 수는 없는 노릇이었습니다. 또한 이거…자주 쓰게 될 거다! 라는 직감이 꼳혀 제 전두엽을 자극했습니다.

까짓 거 한 번 해보죠
까짓 거 한 번 해보죠

못 할 거 없지라는 마음을 가지고 annotate를 사용하기로 했습니다. 그렇게 annotate를 이용해 코드를 작성한다면,

from django.db.models import Subquery, CharField, OuterRef
subject_title = Subquery(
    Subject.objects.filter(user=OuterRef('user')).values('title')[:1],ouput_field=CharField()
    )
queryset = Profile.objects.annotate(
    subject_title=subject_title
    ).all()

# serializers.py
class ProfileSerializer(serializers.ModelSerializer):
    subject_title = serializers.CharField()

    class Meta:
        model = Profile
        fields = ['name', 'subject_title']

위의 코드와 같이 작성할 수 있습니다. annotate를 통해 subject_title이라는 field가 추가되고, serializer에는 field를 선언하는 것으로 사용할 수 있습니다.

쿼리 삼형제


이렇게 코드를 짜기까지는 그리 순탄치는 않았습니다. 하지만 한 번 감이 잡히니 자주 사용하게 되더라구요.

특히 annotate를 사용하면서 자주 쓰게 된 함수들이 있습니다. 바로 OuterRef, Value입니다.

쿼리 삼형제다
쿼리 삼형제다

annotate가 저 삼형제에 포함되어 있는 이유는 다른 함수와 뗄 수 없는 관계이기 때문에 가족! 형제! 그러니까 삼형제! 가 되었습니다.

annotate와 함께 자주 사용하는 OuterRef는 Django에서 제공하는 쿼리 함수로, 외부 테이블의 데이터에 접근합니다. 즉, OuterRef를 사용해 annotate로 field를 추가하려는 모델의 데이터를 참조해 다른 모델의 데이터를 조회할 수 있습니다.

특정 user를 받아올 뿐만 아니라 profile의 name 또한 가져와 annotate로 field에 추가하자고 한다면,

u = User.objects.annotate(profile_name=Profile.objects.filter(user__id=OuterRef('id'))).get(id=1)

이런 식으로 코드를 짤 수 있습니다. 하지만 해당 코드를 실행할 경우 아래와 같은 에러가 발생합니다.

django.db.utils.ProgrammingError: subquery must return only one column

하나의 값이 아닌 여러 값이 넘어와 에러가 났다는 메세지입니다. filter같은 경우 object 형태가 아닌 Queryset dict 형태로 오기때문에 에러가 발생했습니다.

그럼 get으로 바꾸면 되지 않을까?
그럼 get으로 바꾸면 되지 않을까?

그렇다면 get을 쓴다면 어떻게 될까요?

ValueError: This queryset contains a reference to an outer query and may only be used in a subquery.

에러가 뜹니다.

이런 에러가 발생하는 이유는 OuterRef같은 경우 subquery[1]에서만 사용 가능하기 때문에 queryset 형태가 아닌 object를 반환하는 get에서는 사용할 수 없습니다.

OuterRef를 통해 가져온 queryset을 사용하기 위해서는 values를 이용해, 사용하고자 하는 데이터를 특정시킨 뒤, 인덱싱으로 데이터를 가져와야합니다.

u = User.objects.annotate(profile_name=Profile.objects.filter(user__id=OuterRef('id')).values('name')[:1]).get(id=1)

코드를 보다 보기 편하게 subquery에 별칭을 부여할 수도 있습니다.

profile_name = Profile.objects.filter(
    user__id=OuterRef('id')
)
u = User.objects.annotate(
    profile_name=profile_name.values[:1]).get(id=1)

Value는 정수 혹은 불린 값 등 특정 값을 넘겨줄 때 사용할 수 있습니다.

단순히 profile_name 필드에 ‘user’라는 값을 넣는다면 Value를 통해 이렇게 작성할 수 있습니다.

u = User.objects.annotate(
    profile_name=Value('user', output_field=CharField())).get(id=1)

Value는 넘겨주는 데이터가 어떤 형태의 값인지 알려줘야 합니다. 매개변수인 output_field는 model을 작성할 때 사용하는 Field를 사용할 수 있습니다.

하지만 Django 공식에서는 Value를 사용할 때 권고사항을 안내하고 있습니다.

(생략)…You will need to use Value() when you want to pass a string to an expression.

(생략)…문자열을 표현식으로 넘기고 싶을 때 Value()를 사용해야 합니다.

Django에서 문자열을 넘길 경우 필드 이름으로 인식하기 때문에 Value를 사용할 것을 권장하고 있습니다.

글을 맺으며…


저번 글이 회사 내부에서 반응이 좋았다보니 더 재밌게 써야만 해…! 하는 생각이 들기도 했었어요.

하지만 그래봤자 별로 좋은 글이 나오지 않는 걸 알고 있기 때문에 자료도 열심히 찾아 작성해봤습니다.

이 글을 통해 조금이라도 subquery가 쉬워지셨기를 바랍니다.😚

주석


1: SQL문에 포함되어 있는 또 다른 SQL문을 뜻함

amy's profile image

amy

2021-04-26 17:00

Read more posts by this author