Django 에서 Multiple Filter 만들기

Jeonghoon·2022년 7월 24일
0

django

목록 보기
1/3

Django 를 사용하면 filter 관련 패키지는 대부분 django-filter 라는 패키지를 사용한다.

모든 사용자들이 그러겠지만 어떠한 패키지를 사용할 때 패키지가 주는 좋은 점도 있지만 반대로 패키지가 제약을 걸어서 불편한 점이 존재한다

저에게 있어서 django-filter 가 주는 불편한 점은 multiple filter 관련의 choices 와 queryset 과 모든 필드를 커버 못하는 multiple filter 였다.

# 기본적인 코드
class UserFilter(django_filters.FilterSet):
    username = filters.CharFilter()
    login_timestamp = filters.IsoDateTimeFilter(field_name='last_login')
    
# multiple 필터를 구현하기 위하 조건들 
# (field_name, to_field_name, queryset(choices) 이 필요
class FooFilter(BaseFilterSet):
    foo = django_filters.filters.ModelMultipleChoiceFilter(
        field_name='attr__uuid',
        to_field_name='uuid',
        queryset=Foo.objects.all(),
    )

여기에서 불편한 점이 존재한다.

  1. 필요이상의 길어진 코드
  2. queryset이 들어감으로 인해서 해당 queryset이 evaluate 가 될 경우 성능이슈
  3. queryset 혹은 choices 를 제공하기 어려운 필터는 multiple 필터를 사용하기 어렵다

그래서 해당하는 불편함을 개선하기 위해서 커스텀해서 사용했었는데 코드는 아래와 같다.

from django.utils.translation import gettext as _
from django_filters import rest_framework as filters
from django import forms
from django.core.exceptions import ValidationError


class MultipleField(forms.Field):
    widget = forms.MultipleHiddenInput
    default_error_messages = {
        'invalid': _('Enter a valid data.'),
    }

    def __init__(self, coerce=None, *args, **kwargs):
        self.coerce = coerce
        super().__init__(*args, **kwargs)

    def validate(self, value):
        if self.coerce and isinstance(value, list):
            try:
                value = [self.coerce(v) for v in value]
            except ValueError:
                raise ValidationError(self.error_messages['invalid'], code='invalid')
        return value


class MultipleFilter(filters.MultipleChoiceFilter):
    field_class = MultipleField

코드는 어떻게 보면 간단하다.

MultipleChoiceFilter 를 상속받고 필드 클래스를 커스텀 한 MultipleField 로 설정해주면 끝이다.

왜 이렇게 구현할 수 있는지는 기존 MultipleChoiceFilter 를 보면 편하다.

class MultipleChoiceFilter(Filter):
    field_class = MultipleChoiceField
    # 여기에서 필드 클래스를 더 타고 들어가면
    
class MultipleChoiceField(ChoiceIteratorMixin, forms.MultipleChoiceField):
    iterator = ChoiceIterator
    # 이렇게 나오는데 그러면 MultipleChoiceField 를 들어가면 어떻게 동작하는지를 알 수 있다. 

class MultipleChoiceField(ChoiceField):
    hidden_widget = MultipleHiddenInput
    widget = SelectMultiple
    default_error_messages = {
        "invalid_choice": _(
            "Select a valid choice. %(value)s is not one of the available choices."
        ),
        "invalid_list": _("Enter a list of values."),
    }
    # 기본적으로 hidden_widget 에서 multiple 에 관련된 것을 제공하고 제약적인 부분은 choicefield를 통해서 상속받는다.
    # 이것이 확증이 되려면 ChoiceField 가 원하는 동작을 하는지 확인하면 된다.

class ChoiceField(Field):
    widget = Select
    default_error_messages = {
        "invalid_choice": _(
            "Select a valid choice. %(value)s is not one of the available choices."
        ),
    }

    def __init__(self, *, choices=(), **kwargs):
        super().__init__(**kwargs)
        self.choices = choices
        # 여기서 init 을 보면 choices 를 받는 것이 보이고 invalid 한 경우 해당하는 에러를 띄우는 것을 볼 수 있다.
        # 그렇다면 여기에서 필드만 다른 것을 상속받으면 된다는 것을 알 수 있고 해당 부분의 경우 나는 forms.Field 제일 기본적인 부분을 상속받았다.
        

그래서 결국에는 어떻게 코드가 개선되었는 지를 보면

class StateFilter(filters.FilterSet):
    big_cities = MultipleFilter(field_name='big_cities__name', coerce=str)
    small_cities = MultipleFilter(field_name='big_cities__small_cities__name', coerce=str)

이렇게 변경이 되어졌다.

내가 본 장점은 이렇다.

  1. 불필요한 choices 혹은 queryset이 사라졌다.
  2. field_name 을 통한 필터로 다른 필터와의 통일감
  3. 모든 컬럼에 대해 multiple 한 접근이 가능해졌다.

그리고 반대로 단점도 존재하는데
1. 필드에 대한 제약조건이 없음으로 인해서 validation check 가 없어짐

나는 해당부분을 간단하게 해결을 하기위해서 coerce를 제공하는 방향으로 구현을 했다.

좀 더 다양하게 구현하고 싶은 경우에는 내가 생각했을 때 두 가지 방법이 있다.

  1. field를 다양하게 상속받아서 사용한다.
  2. coerece 보다 포괄적으로 체크할 수 있는 인자를 받는 것이다.

이렇게 하면 좀더 의미에 맞는 multiple filter들을 만들 수 있을 것이다.

만약에 간단하게 경험하고 싶으면django-custom-filter 를 보면 될 것 같다.

profile
개발중

0개의 댓글