Django M:N 필드로 order_by 걸기 (annotate, Subquery, OuterRef 사용)

김정우·2022년 2월 23일
0

django

목록 보기
1/1
post-custom-banner

첫 직장 입사 8개월차 주니어 프론트엔드 개발자의 업무 일지 겸 기록입니다.
혹시 내용을 잘못 알고 있다거나, 더 좋은 방법이 떠오르신다면 댓글에 남겨주세요.
정말 많은 도움이 될 거에요. 미리 감사드립니다! 🙏

결론

정보

장고에서 order_by 를 사용하면 모델의 쿼리셋을 특정 필드를 기준으로 정렬한다.

하지만 정렬하고자 하는 모델과 M:N 관계를 맺는 모델의 many-to-many 필드를 기준으로 삼는 경우 정렬 후 중복된 데이터가 생길 수 있다.
(M:N이기 때문에) 여러 객체와 동시에 연결이 되어 있는 경우 어떤 객체를 기준으로 정렬을 해야할 지 알 수 없기에 해당 객체 각각에 대한 데이터를 모두 만들어 정렬하기 때문이다.

이러한 문제를 해결하기 위해 Subquery를 사용하여 m2m 관계인 모델의 객체 중 원하는 객체의 정보만 담은 annotate 필드를 만들었고 최종적으로 이 값을 기준으로 order_by 메서드를 실행하여 중복된 데이터를 제거하는데 성공했다.

느낀점

  1. 다양한 방법을 계속 떠올리자. 문제의 원인이 처음에 생각했던 원인 중에 있을 것이라고 단정해서 결국 문제를 해결하는 시간이 훨씬 길어졌다.
  2. 스택오버플로우는 정답이 아니다. 아이디어만 얻고 문서를 통하거나 다른 방법으로 내가 얻은 아이디어가 맞는지 검증을 거쳐야 한다. 항상 결국 모자란 건 시간이기 때문에 쉽지는 않겠지만, 스택오버플로우나 구글링을 통해 얻은 지식이 사실이라고 단정하는 생각은 경계해야 한다는 생각이 들었다.

추후 액션

공부하고나면 정리 후 링크로 연결해 둘 생각이다.

1. count와 len의 차이에 대해서 알아보자. 프론트에서 쿼리셋의 count를 찍었을 때와 for 루프를 돌 때 실제 쿼리셋의 객체 갯수가 달랐던 점, for문 후에 count를 다시 찍어보면 값이 바뀌어 있던 점이 제일 혼란스러웠던 부분이다. 아마도 count는 실제 데이터를 로드하지 않고, for 루프를 돌 때 실제로 쿼리가 실행되면서 캐싱되는 과정을 놓쳐 혼란스러웠던 것 같다. 이 부분을 다시 면밀하게 공부해보자.

문제를 겪고 해결한 과정을 조금 더 자세하게 적어보고자 한다.


개발을 하던 도중 의사 모델의 쿼리셋을 정렬해야 할 일이 생겼다.
정렬 기준은 의사 모델과 M:N 관계로 결합된 Specialty 모델의 name 필드.

한참 뒤에 알았지만 수시간 동안 계속된 삽질의 시작은 큰 고민 없이 작성한 아래의 정렬 코드였다.
sorted_doctors = doctors.order_by('specialties', 'name')

그리고 나서 프론트 작업을 하는데 웬걸, 일부 데이터가 중복으로 포함되어 있어 실제 데이터의 수보다 더 많은 데이터가 렌더링되고 있었다.
의심되는 부분이 두 가지가 있었다.
1. 병원의 의사 데이터 설정이 잘못되었다.
2. 프론트 코드 어딘가에서 잘못된 로직 등의 이유로 데이터가 복사되고 있다.

병원의 의사 데이터 설정이 잘못되었다.
처음에는 병원의 의사 데이터가 잘못(중복되어) 설정되어 있다고 생각했다. 하지만 그것은 나의 바람이었을뿐. 어드민을 통해 확인한 결과 금방 데이터 설정에는 문제가 없음을 알 수 있었다.

다른 가능성을 생각하지 못하고 두 가지 의심 중 하나가 원인이라고 생각하고 있었기에 이후 2번 의심이 원인이라고 확정하게 된다....

프론트 코드 어딘가에서 잘못된 로직 등의 이유로 데이터가 복사되고 있다.

그 다음으로는 프론트에서 실수로 데이터를 복사하는 부분이 있을거라고 의심하고 한참을 뒤져보았다.

원인을 알 수 없었고, 오히려 이상한 점은 데이터에 .count 를 찍어보면 정상적인 데이터의 갯수가 나오고 있다는 점이었다.

그러니까 데이터의 갯수는 문제가 없다고 나오는데 해당 데이터로 for 루프를 돌리면 실제 데이터 갯수 + 중복된 데이터 갯수 만큼 돌아가는 것이었다.

정말 알쏭달쏭한 문제였다. 또 장고와 (특히) 장고 템플릿에 익숙하지 않았기에 내가 모르는 무언가가 작용하여 데이터가 증가했다고 생각했고, 구글링과 장고 독스를 뒤지며 해결책을 찾으려고 애썼다.

결국 한참을 고민하다가, 어쨌든 정렬이 된 데이터임에도 쿼리셋에 중복 데이터의 인덱스가 서로 달랐다는 점이 의아해서 정렬코드를 다시 확인해보았다. 그리고 구글링을 하기 시작했고 나와 같은 문제를 겪고 있다는 글과 문제의 원인을 발견했다.

질문

답변

(출처 https://stackoverflow.com/questions/23600299/order-by-on-many-to-many-field-results-in-duplicate-entries-in-queryset)

그리고 나서 어드민을 통해 다시 데이터를 확인해보니 이제서야 원인이 분명해졌다. 중복으로 데이터가 생성된 의사 객체의 경우 연결된 Specialty가 2개였다.

그 이후로는 해야할 것이 분명해서 독스를 뒤지기 시작했다.
1. 우선 annotate로 중복된 값이 없는 필드를 만들고 이 값을 기준으로 정렬해줘야겠다고 생각했다.
doctors.(first_specialty="").order_by('first_specialty')


2. 그리고 나서는 first_specialty 에 넣어줄 값을 만들어야 했는데, 연결된 Specialty 객체가 몇 개이든 간에 내가 원하는 조건을 만족하는 객체의 이름을 넣어주면 되었다.
찾아보니 장고의 Subquery 메서드를 사용하면 쿼리셋 안에서 쿼리를 또 할 수가 있었고, annotate와 함께 사용되는 경우가 많은 듯 하였다.

이와 관련한 좋은 예제가 독스에 있어 기록한다.

from django.db.models import OuterRef, Subquery
newest = Comment.objects.filter(post=OuterRef('pk')).order_by('-created_at')
Post.objects.annotate(newest_commenter_email=Subquery(newest.values('email')[:1]))

나의 코드는 이렇게 수정되었다.

doctors.annotate(
	first_specialty = Subquery(
    	Specialty.objects.filter(doctors__id=OuterRef('id')).values('name')[:1]
	)
).order_by('first_specialty')

여기서 OuterQuery는 Subquery의 바깥 쿼리의 필드를 참조할 수 있게 해준다. 그러니까 위의 코드에서는 OuterRef('id')를 통해 의사 객체의 id 값을 참조할 수 있는 것이다. 작성한 서브쿼리를 통해 첫번째 Specialty 객체의 name을 first_specialty 필드에 저장할 수 있게 되었고, 이 값을 기준으로 정렬을 하니 중복되었던 데이터가 없어졌다.


출처

스택오버플로우 질문
Django docs

profile
hello world!
post-custom-banner

0개의 댓글