Python (with. Code)/Django-ninja

[Django Ninja] 커스텀 Pagination 모듈 구현기

yubi5050 2023. 4. 16. 18:12

Django Ninja에서의 Pagination은 Decorator 선언 만으로 동작 될 수 있도록 유저 편의성이 매우 좋다.

(유저 편의성이 좋다는 건 대신 그만큼 커스텀하기 까다롭다는 것..) 

 

이번 글에서는 공식 Docs에서 제공하는 일반적인 Pagination 방법 2가지를 간단히 실행해보고,

추가로 직접 CustomPagination을 구축한 방법에 대해 공유 해보려고 한다.

(해당 글은 ninja 0.21.0 버전을 기준으로 작성하였음)

 

참고 : https://django-ninja.rest-framework.com/guides/response/pagination/

 

PageNumber 기반 페이지네이션

ninja.pagination 모듈에서 제공하는 PageNumber 기반 페이지네이션 방법이다.

  • 방법 : @paginate(PageNumberPagination, page_size)의 데코레이터 (page_size는 한 페이지에 나타날 데이터 크기)
  • API : GET /api/users?page=1  (page는 페이지 번호, 쿼리스트링 방식)
  • Return 값 : 페이지네이션으로 분할 될 쿼리셋 목록을 리턴 - all(), filter() 등
  • Response : List [ ] 형태 필수

 

from ninja.pagination import paginate, PageNumberPagination

@api.get('/users', response=List[UserSchema])
@paginate(PageNumberPagination, page_size=5)
def list_users(request):
    return User.objects.all()

 

LimitOffset 기반 페이지네이션

ninja.pagination 모듈에서 제공하는 LimitOffset 기반 페이지네이션 방법이다.

  • 방법 : @paginate(LimitOffsetPagination) 의 데코레이터 선언
  • API : GET /api/users?limit=5&offset=1 (limit은 한 페이지에 나타낼 크기, offset은 특정 지점, 쿼리스트링 방식))
  • Response : List [ ] 형태 필수

 

from ninja.pagination import paginate, LimitOffsetPagination

@api.get('/users', response=List[UserSchema])
@paginate(LimitOffsetPagination)
def list_users(request):
    return User.objects.all()

 

커스텀 페이지네이션 모듈 만들기 - 페이지네이션 모듈 분석

ninja에서 제공되는 페이지네이션 방식 예제는 정말 간편하다.

다만 응답 형태가 고정적이라(items, count), 추가 필드 포함시 기존 Schema를 상속 받아 커스텀하는 작업이 필요하다.

 

우선 페이지네이션 모듈을 이해하기 위해선 크게 두 가지 클래스를 살펴보았다.

  • PageNumberPagination Class (or LimitOffsetPagination Class)
  • PaginationBase (추상 Class)

 

PageNumberPagination 특징

  • PaginationBase를 상속받아 Input 클래스와, paginatie_queryset 메소드를 오버라이드 한다.
  • page_size는 기본값으로 settings.PAGINATION_PER_PAGE 값을 따른다.
  • paginate_queryset 의 역할은 return 된 queryset들을 슬라이싱 하는 역할을 수행한다. (진짜 쿼리가 일어나는 부분)
  • LimitOffsetPaginiation 코드 모듈을 작성하진 않았지만 살펴보면 유사한 구성으로 되어있다.

 

class PageNumberPagination(PaginationBase):
    class Input(Schema):
        page: int = Field(1, ge=1)

    def __init__(
        self, page_size: int = settings.PAGINATION_PER_PAGE, **kwargs: Any
    ) -> None:
        self.page_size = page_size
        super().__init__(**kwargs)

    def paginate_queryset(
        self,
        queryset: QuerySet,
        pagination: Input,
        **params: DictStrAny,
    ) -> Any:
        offset = (pagination.page - 1) * self.page_size
        return {
            "items": queryset[offset : offset + self.page_size],
            "count": self._items_count(queryset),
        }  # noqa: E203
        
# 위 PageNumberPagination과 비슷
class LimitOffsetPagination(PaginationBase)
	....

 

PaginationBase 특징 

  • PaginationBase는 추상클래스로 선언되어 있다.
  • Output에 대한 기본 Schema 선언이 되어 있다.
  • InputSource로 limit, offset / page 등의 쿼리 스트링 인자들을 Schema 형태로 받을 수 있게 해준다.
  • items_attribute : 응답값에 대한 key 값이 정의 되어 있다. 
  • 그 밖에 pagination_queryset에 대한 추상메소드와, _items_count() 메소드가 구현되어 있다.

 

# ninja/pagination.py

class PaginationBase(ABC):
    class Input(Schema):
        pass

    InputSource = Query(...)

    class Output(Schema):
        items: List[Any]
        count: int

    items_attribute: str = "items"

    def __init__(self, *, pass_parameter: Optional[str] = None, **kwargs: Any) -> None:
        self.pass_parameter = pass_parameter

    @abstractmethod
    def paginate_queryset(self, queryset: QuerySet, pagination: Any, **params: DictStrAny,) -> Any:
        pass  # pragma: no cover

    def _items_count(self, queryset: QuerySet) -> int:
        try:
            # forcing to find queryset.count instead of list.count:
            return queryset.all().count()
        except AttributeError:
            return len(queryset)

 

결론

결론적으로 내가 어떤 부분을 커스텀하고 싶냐에 따라 모듈을 적절히 상속받아 재선언해야 되는지가 커스텀의 핵심인 것 같다.

 

공식 docs에서는 일반적으로 PaginationBase 추상 클래스를 상속받고 Input(), Output(), pagination_queryset 등을 직접 구현하는 것을 권장하는 것으로 보인다.

 

최종적으로 작성한 페이지네이션 모듈은 다음과 같다.

  • PaginationByPageIn : page = Field(~~) 구체적 선언, validator를 이용한 validation 구현
  • PaginationByPageOut : items, count 외에 msg, code 추가 / count는 total_count로 이름 변경
  • PaginationByPageOut: page_size에 대한 필드도 주어, 클라이언트에서 데이터 크기를 조정 가능
  • pagination_queryset : 오버라이드한 메소드에 넘겨줄 Output Schema에 대한 필드들을 채워 dictionary 형태로 리턴하였다. (dictionary 형태로 작성되어도 됨.)

 

class CustomPaginationByPage(PaginationBase):
    # Input Params - ex) page / limit, offset / cursor
    class Input(PaginationByPageIn):
        pass

    # Output Params - ex) items, count, etc..
    class Output(PaginationByPageOut):
        pass

    def paginate_queryset(self, queryset, pagination: PaginationByPageIn, **params):
        page_size = params["params"].page_size
        offset = (pagination.page - 1) * page_size

        # out
        out = self.Output()
        out.items = queryset[offset : offset + page_size]
        out.total_count = self._items_count(queryset)
        out.page_size = page_size
        out.success(ResultCode.Success) # msg와 code 작성해주는 함수
        return out.dict()

 

추가 

이 밖에도 추가 커스텀이 필요할 경우 방향성을 예상해보자면..

  • queryset의 lazy_load 특성에 따라 슬라이싱을 좀 더 다르게 하고 싶을 때 => paginate_queryset을 수정
  • cursor 방식으로 수정하고 싶을 때 => paginate_queryset을 id 기반으로 수정
  • 응답 필드를 items에서 results 등으로 바꾸고 싶을 때 => items_attribute 값을 상속 받아 수정
  • counting을 조금 다른 방식으로 진행하고 싶을 때 => _items_count() 등을 수정
  • ...

 

등이 있을 것 같다.