django-filter용 NULLS LAST 정렬 필터.

이 글 내용은 PostgreSQL을 기반으로 하며, 다른 RDBMS 에서는 확인 안 해봤다.

Django ORM으로 NULLS LAST 정렬

Hannal이라는 모델이 있고 name 필드는 null을 허용한다.

class Hannal(models.Model):
    name = models.CharField(max_length=32, null=True, blank=True)
    birthday = models.DateField(null=True, blank=True)

여러 데이터의 name 필드의 값이 다음과 같이 들어갔다고 가정하자.

  • 'abc'
  • 'zyx'
  • None
  • 'lmn'

이제 name 필드로 오름차순(Ascending) 정렬해보자.

Hannal.objects.order_by('name')
  • None
  • 'abc'
  • 'lmn'
  • 'zyx'

결과는 null이 먼저 나온다. 만약 null이 아닌 데이터를 먼저 정렬하고 그 이후에 null인 데이터를 나열하려면 NULLS LAST로 정렬해야 한다.

from django.db.models import F

Hannal.objects.order_by(F('name').desc(nulls_last=True))
  • 'abc'
  • 'lmn'
  • 'zyx'
  • None

우리가 원하는 대로 정렬됐다.

django-filter의 OrderingFilter

이번엔 이를 django-filter에 적용해보자. django-filter에 정렬 필터인 OrderingFilter를 사용하면 간편하게 정렬 필터를 적용할 수 있다.

import django_filters as filters


class HannalFilter(filters.FilterSet):
    ordering = filters.OrderingFilter(
      fields=(
          ('name', 'name', ),
          ('birthday', 'saengil', ),
      ),
    )

자세한 내용은 공식 문서에 나와있지만, 영문 싫어하는 사람도 많고 공식 문서가 썩 친절하진 않으니 설명하겠다.

fields로 모델의 필드와 HTTP Query String에서 넘겨 받을 필드를 짝지어야 한다. 뒤(namesaengil)는 Query string으로 받을 값, 앞(namebirthday)은 모델의 필드/필드표현식을 뜻한다. 보통은 모델 필드명과 동일하게 하는 게 알아보기 좋지만, 다르게 해야 할 경우도 많다.

우선 요청자(client)측에서 다른 이름을 쓰고 싶은 경우이다. Python 관례에 따르면 snake case 표기를 쓰겠지만, Front-end쪽에서는 camel case 표기를 대개 쓴다. 예를 들어 모델 필드명은 birth_day인데 요청자는 굳이 birthDaysaengil을 쓰고 싶은 경우이다.

다른 경우가 더 흔한 경우인데, 모델 관계(model relationship)를 필드 표현식으로 다뤄야 하는 경우이다. 예를 들어 Kay라는 모델이 있고 Hannal 모델과 1:N 관계를 맺고 있다고 하자.

class Kay(models.Model):
    name = models.CharField(max_length=32, null=True, blank=True)


class Hannal(models.Model):
    name = models.CharField(max_length=32, null=True, blank=True)
    birthday = models.DateField(null=True, blank=True)
    related = models.ForeignKey('Kay', on_delete=models.CASCADE)

그리고 Kay 모델의 name 필드를 기준으로 정렬한 Hannal 모델의 데이터를 가져오려면 다음과 같이 한다.

Hannal.objects.order_by('related__name')

여기서 related__name을 django-filter 에서도 사용할 수 있다.

class HannalFilter(filters.FilterSet):
    ordering = filters.OrderingFilter(
      fields=(
          ('name', 'name', ),
          ('birthday', 'saengil', ),
          ('related__name', 'kayname', ),
      ),
    )

이렇게 fields를 지정하면 ordering키의 값으로 name, saengil, kayname을 사용할 수 있다. Django REST framework(이하 DRF)에 적용하면 URL을 ?ordering=name로 하여 name 필드에 대해 오름차순 정렬하거나 ?ordering=-name로 하여 내림차순 정렬한다. 이 뿐만 아니라 여러 개 필드를 정렬할 수도 있다. 예를 들어 name 필드는 오름차순, birthday 필드는 내림차순으로 정렬하고자 한다면 HTTP Query String 으로 ?ordering=name,-birthday라고 하면 된다.

OrderingFilter에 NULLS LAST 적용

편하다. 근데 django-filter 2.4.0 버전 기준까지는 NULLS LASTNULLS FIRST를 지원하지 않는다. 그러므로 따로 구현해야 한다. OrderingFilter가 제공하는 기능은 그대로 쓰되 QuerySet 만들 때 null 정렬만 추가하면 되니 이 클래스를 상속받아 쓴다.

from django.db.models import F
import django_filters as filters


class NullsLastOrderingFilter(filters.OrderingFilter):
    def filter(self, qs, value):
        if not value:
            return qs
        for _v in value:
            is_desc = _v.startswith('-')
            field = self.param_map.get(_v[1:] if is_desc else _v)
            if not field:
                continue
            if is_desc:
                qs = qs.order_by(F(field).desc(nulls_last=True))
            else:
                qs = qs.order_by(F(field).asc(nulls_last=True))
        return qs


class HannalFilter(filters.FilterSet):
    ordering = NullsLastOrderingFilter(
      fields=(
          ('name', 'name', ),
          ('birthday', 'saengil', ),
          ('related__name', 'kayname', ),
      ),
    )

    class Meta:
        model = Hannal
        fields = ('name', 'birthday', )

위에서 설명한 내용을 모두 반영한 코드이다. filter() 메서드는 django-filter 가 넘겨받은 QuerySet 객체이다. DRF에 연동해 사용한다면 DRF가 이런 저런 조치를 취한 QuerySet일테고, 사용자가 만든 filter 들을 거친 QuerySet이기도 하다.

NULLS FIRST를 적용하려면 nulls_last 대신 nulls_firstTrue로 지정하면 된다.