본문 바로가기

web/Django_rest_framework

[Django rest framework] 4. filters

이번 포스팅의 소스는 여기에 있습니다.

 

 

- python3.6, django 2.1, django-rest-framework 3.8을 사용합니다.

 

 

django에서의 filter는 보통 query_set()을 통한, 즉 쿼리를 통해 조건을 추가 하여 데이터를 반환하는 경우가 많습니다.

 

이를 더욱 재사용가능하게 만든 패키지가 django-filter입니다. 이를 활용하면 간단하게 filter / order by를 추가할 수 있습니다.

 

 

Using django-filter

먼저 해당 패키지를 설치 합니다.

pip3 install django-filter

 

settings.py의 rest_framework 의 옵션에 filter사용을 선언합니다.

# settings.py

REST_FRAMEWORK = {
.............
    'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',),
.............
}

 

이제 사용하는 ModelviewSet에 filterset_fields를 선언하고, 필터링하고 싶은 필드 명을 추가하면 됩니다.

해당 모델은 post모델로 모델의 구조는 앞썬 포스팅을 참조해주시기 바랍니다.

저는 post모델의 칼럼중 message의 값을 filter 하려 합니다.

# views.py

class PostViewSet(ModelViewSet):
	filterset_fields = ('message',)

이제 url을 다음과 같이 요청하면 요청한 값만 필터링되어 나오는 것을 볼 수 있습니다.

 

http://127.0.0.1:8080/api/v1/post/?message=post 2

filterset_fields = ()의 선언으로 간편하게 filter를 구현할수 있습니다. 하지만 해당 필터는 equal조건으로 다른 다양한 조건을 위해서는 filterSet을 구현해야 합니다.

 

 

 

 

Declaring filters

다양한 필터 조건을 해보기 위해 칼럼을 추가하려 합니다.

post모델에 public / count 칼럼 추가 합니다.

# models.py

class Post(OwnedModel):
    message = models.TextField(blank=True, help_text="메시지")
    public = models.BooleanField(default=False, help_text="공개 여부")
    count = models.IntegerField(default=0, help_text="읽은 유저 수")
    created_at = models.DateTimeField(auto_now_add=True)

모델 적용을 위해 migrate를 실시 합니다.

$ ./manage.py makemigrations
$ ./manage.py migrate

 

추가된 칼럼의 데이터 변경를 변경합니다.

저는 count / public의 값을 아래와 같이 변경하였습니다.

 

이제 실제 필터를 구현할 차례입니다.

먼저 django_filters의 다양한 필터를 통해 구현합니다. serialzer의 선언과 비슷하다고 생각하시면 됩니다.

필터링의 선언은 크게

  • url에 어떻게 선언할 것인가
  • 어떤 타입으로 받을 것인가
  • 어떤 조건으로 필터링 할것인가

로 요약이 됩니다.

 

count__gt = NumberFilter(field_name="count", lookup_expr="gt")

위를 예로 보면 url에서는 count__gt로 값을 줄 경우 작동합니다.

NumberFilter()는 해당 값은 Numbe형태로만 받을 수 있습니다. 다른 데이터형은 에러를 줍니다.

field_name은 모델에 선언된 칼럼 중 count 칼럼의 값과 비교하며,

lookup_expr은 비교를 어떻게 할 것인가입니다. "gt"의 경우 큰 것을 뜻합니다.

즉 위의 코드는 count__gt로  들어온 값이 있다면 해당 값보다 count가 큰 값만 리턴됩니다.

 

 

 

이제 filters의 전체 소스입니다.

# filters.py

from django_filters import (
    FilterSet,
    CharFilter,
    NumberFilter,
    BooleanFilter,
)

from .models import Post


class PostFilter(FilterSet):
    count = NumberFilter()
    count__gt = NumberFilter(field_name="count", lookup_expr="gt")
    count__lt = NumberFilter(field_name="count", lookup_expr="lt")
    public = BooleanFilter(field_name="public")
    message = CharFilter(lookup_expr="icontains")

    class Meta:
        model = Post
        fields = ["message", "count", "public"]

    def __init__(self, *args, **kwargs):
        super(PostFilter, self).__init__(*args, **kwargs)

 

views.py에는 앞서 선언한 filter를 viewSet에 적용합니다.

# views.py

from .filters import PostFilter

class PostViewSet(ModelViewSet):
......
    filter_class = PostFilter

 

serializer도 변경되었습니다. 앞 썬 포스팅에선 onwer에 대한 정보가 없었으나, owner의 str()를 반환하도록 하였습니다. (유저 객체의 str()은 username을 반환합니다.)

#serializers.py

class PostSerializer(serializers.ModelSerializer):
    owner = serializers.SerializerMethodField()

    def get_owner(self, obj):
        return str(obj.owner)

    class Meta:
        model = Post
        fields = ["id", "message", "created_at", "count", "public", "owner"]

 

이제 준비는 끝났습니다. 결과를 확인해 봅시다.

 

1. /api/v1/post/?message=2 검색

    message에 2가 들어간 데이터가 검색되는 것을 확인할 수 있습니다. filter CharFilter() "icontains"로 검색 조건이 되어 있습니다.

2. /api/v1/post/?count__gt=10 검색

    count의 값 중 10보다 큰 값을 검색합니다.

3. /api/v1/post/?public=False 검색

    public의 값이 False인 값을 검색합니다. filter에서는 BooleanFilter()로 선언되어 있습니다.

 

 


ordering

filter과 같이 ordering도 선언식으로 쓸 수 있습니다.

함수는 OrderingFilter를 사용하며, 해당 url로 받을 변수의 이름을 지정하는 order_by_field, 그리고 ordering을 할 필드 명으로 구분됩니다. 오름 차수는 필드명만, 내림 차수는 -필드명으로 앞에 -를 붙입니다.

 

먼저 filter에 order_by_field / ordering를 선언해 줍니다.

from django_filters.filters import OrderingFilter

class PostFilter(FilterSet):
	...................

    order_by_field = 'ordering'
    ordering = OrderingFilter(
        fields=(
            'message',
            'count',
            'public',
        )
    )

 

 

1. api/v1/post/?public=False&ordering=count 검색

   public로 필터링된 후 count가 오름 차순으로 정렬됩니다.

2. api/v1/post/?public=False&ordering=-count 검색

  public로 필터링된 후 count가 내림 차순으로 정렬됩니다.

 


django-filter을 통해 간편하게 filter / ording를 구현하였습니다. 이로써, 쿼리에 추가적인 작업이 필요 없이 간편하게 filter / ordering를 쓸 수 있게 되었습니다.

 

 

앞 썬 예제에서는 모델 칼럼의 단순 비교만 나왔습니다.

만일 foreign key와 모델로 연결된 칼럼이 있다면 쿼리에서 쓰던 방식과 동일하게 filter를 선언할 수 있습니다.

 

먼저 테스트를 위해 기존의 req.user를 통해서 로그인한 데이터만 보이면 쿼리문을 변경하였습니다.

filter()의 조건을 임시로 제거하였습니다.

# views.py 

class PostViewSet(ModelViewSet):
	.............
    def get_queryset(self):
        return super().get_queryset().filter()

 

filters.py에 owner의 필드로 검색하려 합니다. owner는 user 모델의 foreign key로 되어 있습니다.

만일 user의 username으로 검색하고 싶다면 아래와 같이 lookup_expr를 username__icontains로 하면 됩니다.

#filters.py

class PostFilter(FilterSet):
	..............
	owner__name = CharFilter(field_name="owner", lookup_expr="username__icontains")

              

 

1. api/v1/post/?owner__name=admin 검색

   유저의 이름이 admin으로 등록된 post가 검색됩니다.

 

2. /api/v1/post/?owner__name=test 검색

    유저의 이름이 test로 등록된 post가 검색됩니다.         

 

 

참고 자료

 

https://django-filter.readthedocs.io/en/stable/ref/filterset.html

https://brownbears.tistory.com/96