본문 바로가기

web/Django_rest_framework

[Django rest framework] 3. pagination

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

 

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

 

 

Django는 paginated 즉 “이전/다음”링크를 사용하여 여러 페이지로 나누어진 데이터를 관리하는데 도움이 되는 몇 가지 클래스를 제공합니다. (Paginator 클래스) 또한 DRF에서도 이러한 pagination 기능을 제공하고 있습니다.

 

PageNumberPagination / LimitOffsetPagination

pagination 설정에는 두 가지 방법이 있습니다.

 

  • PageNumberPagination
    • page : 몇 번째 페이지인지 표시해줍니다. 페이지는 1부터 시작합니다.
    • page_size : 한 페이지에 몇 개의 레코드를 보여줄지 표시해줍니다.
  •  LimitOffsetPagination
    • offset : 몇 번째 레코드부터 보여줄지 설정해줍니다. 설정하지 않을 시 첫 번째 레코드부터 보여줍니다.
    • limit : 몇 개의 레코드를 보여줄지 설정합니다. offset 번째 레코드부터 offset+limit-1 번째 레코드까지 보여줍니다.

 

두 가지 모두 페이지네이션 정의 클래스이나, 파라미터와 리턴 값이 다르다고 생각하시면 됩니다.

저는 PageNumberPagination 클래스를 통해 작성하였습니다.

 

 

Pagination 전역 설정

settings.py의 REST_FRAMEWORK에 pagination에 관한 설정을 추가해 줍니다.

# settings.py

REST_FRAMEWORK = {
    ....
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
    ‘PAGE_SIZE’: 2,
    ...
}

 

 

그리고 이전 포스팅에서 views.py에 작성한 list 부분을 삭제하고, get_queryset()으로 변경해 줍니다.

해당 pagination의 동작은 ModelViewSet의 pagination_class에서 get_queryset()의 쿼리를 기반으로 가져오기 때문에 list() 선언 시 오버라이딩 되면서 작성한 list()로 동작되어 pagination_class는 동작하지 않습니다.

    def get_queryset(self):
        return super().get_queryset().filter(owner=self.request.user)
        

    # def list(self, request, *args, **kwargs):
    #     posts = self.get_queryset().filter(owner=request.user)
    #     serializer = self.get_serializer(posts, many=True)
    #     return Response({"results": serializer.data})

 

 

이제 아래와 같이 파라미터를 추가한 후 API를 호출해 보면 리턴 값이 변경된 것을 볼 수 있습니다.

 

http://127.0.0.1:8080/api/v1/post/?page=1

 

리턴 값에서 count는 전체 객체의 숫자를 뜻하며, next / previous는 다음/이전 페이지의 링크를 전달해 줍니다.

 

 

Pagination 개별 설정

이번엔 전역 pagination의 설정이 아닌 API별 설정을 해 보도록 하겠습니다.

먼저 pagination.py를 만들어 줍니다. 그리고 아래와 같이 CustomResultsSetPagination()를 추가해 줍니다.

보시는 바와 같이 PageNumberPagination 클래스를 상속받고 있으며, 자세한 옵션과 함수는 PageNumberPagination를 보시면 됩니다.

# pagination.py

# -*- coding: utf-8 -*-
from rest_framework.pagination import PageNumberPagination


class CustomResultsSetPagination(PageNumberPagination):
    page_size = 100
    page_size_query_param = 'page_size'

 

이번에 위의 선언한 CustomResultsSetPagination를 views.py에 pagination_class 파라미터에 추가해줍니다.

# views.py

.........
from .pagination import CustomResultsSetPagination
.........


class PostViewSet(ModelViewSet):
	.....
	pagination_class = CustomResultsSetPagination

 

이제 API를 호출해 봅니다. 이전에 호출한 API에서 파라미터가 추가된 것을 볼 수 있습니다.

 

http://127.0.0.1:8080/api/v1/post/?page_size=2&page=1

 

 

이렇게  PageNumberPagination를 상속받아 선언함으로써 pagination을 커스텀할 수 있습니다.

 


pagition 직접 구현하기

직접 구현하기라 썼지만, 상속받았던 PAgeNumberPagination 부분을 오버 라이딩하는 것을 알아보도록 하겠습니다.

 

먼저 PageNumberPagination 클래스를 살펴보면 BasePagination을 상속받은 것을 볼 수 있습니다.

# rest_framework/pagination.py

class PageNumberPagination(BasePagination):
    """
    A simple page number based style that supports page numbers as
    query parameters. For example:
......

 

다시 BasePagination 클래스를 살펴보면 paginate_queryset()와 get_paginated_response()가 NotImplementedError로 선언된 것으로 보아 추상 클래스로 구현해야 하는 것을 볼 수 있습니다.

 

(to_html()는 클라이언트 화면에서 사용 시 호출되는 함수로 api로 사용하는 현재의 프로젝트에서는 구현하지 않아도 됩니다.)

# rest_framework/pagination.py

class BasePagination:
    display_page_controls = False

    def paginate_queryset(self, queryset, request, view=None):  # pragma: no cover
        raise NotImplementedError('paginate_queryset() must be implemented.')

    def get_paginated_response(self, data):  # pragma: no cover
        raise NotImplementedError('get_paginated_response() must be implemented.')

    def get_paginated_response_schema(self, schema):
        return schema

    def to_html(self):  # pragma: no cover
        raise NotImplementedError('to_html() must be implemented to display page controls.')

    def get_results(self, data):
        return data['results']

    def get_schema_fields(self, view):
        assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`'
        return []

    def get_schema_operation_parameters(self, view):
        return []

 

 

이제 pagination.py에 구현해 보도록 하겠습니다.

# -*- coding: utf-8 -*-
from rest_framework.pagination import PageNumberPagination
from django.core.paginator import InvalidPage
from rest_framework.exceptions import NotFound
from rest_framework.response import Response
from collections import OrderedDict


class CustomMakePagination(PageNumberPagination):
	page_size_query_param = 'page_size'

    def paginate_queryset(self, queryset, request, view=None):
        # page_size를 가져옵니다.
        page_size = self.get_page_size(request)
        if not page_size:
            return None
        """
        django_paginator_class는 django의 Paginator (django/core/paginator.py) 클래스로 pagination의 전체 동작을 담당합니다. 
        page_size와 queryset을 pagniator 객체를 선언합니다. 
        """
        paginator = self.django_paginator_class(queryset, page_size)
        page_number = request.query_params.get(self.page_query_param, 1)

        try:
            # 가져온 paginator중에서 사용자가 지정한 페이지를 가져 옵니다. 
            self.page = paginator.page(page_number)
        except InvalidPage as e:
            msg = self.invalid_page_message.format(
                page_number=page_number, message=str(e)
            )
            raise NotFound(msg)

        return list(self.page)

    def get_paginated_response(self, data):
        # pagination의 리턴시 보여줄 데이터 객체 입니다. 
        return Response(OrderedDict([
            ('count', self.page.paginator.count),
            ('results', data)
        ]))

 

이제 위의 페이지네이션 구현 클래스를 Modelsets에 적용합니다.

# views.py

.........
from .pagination import CustomMakePagination
.........


class PostViewSet(ModelViewSet):
	.....
	pagination_class = CustomMakePagination

 

이전과 동일한 API를 호출해 보면 리턴되는 값이 달라진 것을 확인할 수 있습니다.

 

http://127.0.0.1:8080/api/v1/post/?page=1&page_size=1

 

 

이처럼 PageNumberPagination 클래스를 오버라이딩 함으로써 필요한 부분을 커스텀하여 직접 구현할 수 있습니다.