drf에서 제공해주는 cursor pagination을 사용하기에 앞서 커서라는 개념부터 알고가야 한다.
커서 (Cursor)는 컴퓨터로 문서를 작성해 본 사람이라면 누구나 알고 있는 용어이다. 화면에서 현재 사용자의 위치를 나타내며 깜빡거리는 막대기가 바로 커서이다.
데이터베이스에서의 커서 또한 유사한 개념이다. 방대한 양의 데이터에서 특정 위치, 특정 로우(row)를 가리킬때 커서가 사용된다. 위키피디아에서는 커서에 대해 '많은 로우 중 한 로우를 가리키는 포인터와 같다'고 설명하고 있다.
즉 커서란 현재 작업중인 레코드를 가리키는 오브젝트이다.
왜 커서를 쓸까?
우리가 흔히 테스트를 할 때에는 데이터가 많아봤자 100개를 넘어가지 않는다.
만약 cursor를 사용하지 않고 목록을 불러오는 api를 호출한다면 100개의 데이터를 한번에 불러오는 쿼리문을 실행할 것이다.
100개정도 되는 선에서는 무리가 없다. 하지만 그 이상의 대용량 데이터를 불러오게 되면 서버 자원을 갉아먹는 원인이 된다.
그래서 우리는 이 많은 데이터들을 페이지 별로 나눈 후, 사용자가 특정 페이지를 볼 때 그 페이지에 해당하는 데이터들만 불러오게 해주는데 가장 큰 역할을 하는것이 커서이다.
여기서 커서는 이전 페이지의 마지막 데이터를 추적하고, 다음 페이지를 검색할 때 사용한다.
drf에서 제공하는 pagination 기법은 3개가 있다.
이 중 커서기반은 pagination 성능이 향상되고 서버 리소스 사용량이 최소화되는 장점이 있다.
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.CursorPagination',
'PAGE_SIZE': 10,
}
from rest_framework import status, pagination
class ProductPagination(pagination.CursorPagination):
page_size = 10
ordering = "-created_at"
cursor_query_param = "cursor"
def get_paginated_response(self, data):
if self.get_previous_link() is None:
return Response(
{
"meta": {"code": 301, "message": "OK"},
"data": {
"next": self.get_next_link(),
"previous": self.get_previous_link(),
"products": data,
},
},
status=status.HTTP_301_MOVED_PERMANENTLY,
)
else:
return Response(
{
"meta": {"code": 200, "message": "OK"},
"data": {
"next": self.get_next_link(),
"previous": self.get_previous_link(),
"products": data,
},
},
status=status.HTTP_200_OK,
)
class CursorPagination(BasePagination):
"""
The cursor pagination implementation is necessarily complex.
For an overview of the position/offset style we use, see this post:
https://cra.mr/2011/03/08/building-cursors-for-the-disqus-api
"""
cursor_query_param = 'cursor'
cursor_query_description = _('The pagination cursor value.')
page_size = api_settings.PAGE_SIZE
invalid_cursor_message = _('Invalid cursor')
ordering = '-created'
template = 'rest_framework/pagination/previous_and_next.html'
def paginate_queryset(self, queryset, request, view=None):
self.page_size = self.get_page_size(request)
if not self.page_size:
return None
self.base_url = request.build_absolute_uri()
self.ordering = self.get_ordering(request, queryset, view)
self.cursor = self.decode_cursor(request)
if self.cursor is None:
(offset, reverse, current_position) = (0, False, None)
else:
(offset, reverse, current_position) = self.cursor
# Cursor pagination always enforces an ordering.
if reverse:
queryset = queryset.order_by(*_reverse_ordering(self.ordering))
else:
queryset = queryset.order_by(*self.ordering)
# If we have a cursor with a fixed position then filter by that.
if current_position is not None:
order = self.ordering[0]
is_reversed = order.startswith('-')
order_attr = order.lstrip('-')
# Test for: (cursor reversed) XOR (queryset reversed)
if self.cursor.reverse != is_reversed:
kwargs = {order_attr + '__lt': current_position}
else:
kwargs = {order_attr + '__gt': current_position}
queryset = queryset.filter(**kwargs)
# If we have an offset cursor then offset the entire page by that amount.
# We also always fetch an extra item in order to determine if there is a
# page following on from this one.
results = list(queryset[offset:offset + self.page_size + 1])
self.page = list(results[:self.page_size])
# Determine the position of the final item following the page.
if len(results) > len(self.page):
has_following_position = True
following_position = self._get_position_from_instance(results[-1], self.ordering)
else:
has_following_position = False
following_position = None
if reverse:
# If we have a reverse queryset, then the query ordering was in reverse
# so we need to reverse the items again before returning them to the user.
self.page = list(reversed(self.page))
# Determine next and previous positions for reverse cursors.
self.has_next = (current_position is not None) or (offset > 0)
self.has_previous = has_following_position
if self.has_next:
self.next_position = current_position
if self.has_previous:
self.previous_position = following_position
else:
# Determine next and previous positions for forward cursors.
self.has_next = has_following_position
self.has_previous = (current_position is not None) or (offset > 0)
if self.has_next:
self.next_position = following_position
if self.has_previous:
self.previous_position = current_position
# Display page controls in the browsable API if there is more
# than one page.
if (self.has_previous or self.has_next) and self.template is not None:
self.display_page_controls = True
return self.page
.......
def get_paginated_response(self, data):
return Response(OrderedDict([
('next', self.get_next_link()),
('previous', self.get_previous_link()),
('results', data)
]))
{
"next" : "http://127.0.0.1:8000/products/?cursor=cD0yMDIzLTA1LTA5KzEyJTNBMzklM0EzNi45NzY3MzAlMkIwMCUzQTAw",
"previous" : null,
"results" : []
}