Limiting QuerySet (offset, limit) + QuerySets are lazy
QuerySets are lazy (https://docs.djangoproject.com/en/3.0/topics/db/queries/#querysets-are-lazy)
QuerySets are lazy – the act of creating a QuerySet doesn’t involve any database activity. You can stack filters together all day long, and Django won’t actually run the query until the QuerySet is evaluated.
그러니까, 아래와 같이 models.Room.objects.all()[0:5] 같은 코드는 all()을 통해 모두 가져온 후 곧바로 execute되지 않습니다. 위 동작이 실행되는 건 실제로 호출한 후에 작동합니다. 쿼리셋을 만드는 것만으로 DB가 작동하지는 않는다라고 알아두면 될 것 같습니다.
Lazy 로딩이 로딩이 필요한 시점 까지 로딩을 하지 않는 것인 거럼, 쿼리셋도 필요한 시점까지 작동을 하지 않습니다.
거기에 더해서, 똑똑하게도 아래와 같이 models.Room.objects.all()[0:5]는 곧바로 실행하지 않아 (Lazy) 리소스를 낭비하지 않기도 하지만 [0:5]와 같이 offset과 limit을 주었을 때 전부를 불러온 다음 슬라이싱하는 것이라 0부터 시작해서 5개를 가져오는 쿼리가 작성되게 됩니다
from django.shortcuts import render
from . import models
# Create your views here.
def all_rooms(req):
all_rooms = models.Room.objects.all()[0:5]
return render(req, "rooms/home.html", context={"rooms": all_rooms})
이제 url의 query부분에 따라 동적으로 offset/limit를 변화시키면 됩니다.
url의 정보를 request.GET 객체로 부터 get 메서드를 통해 가져옵니다.
(https://docs.djangoproject.com/en/3.0/ref/request-response/)
http://127.0.0.1:8000/?page=2 가 있으면 request.GET.get("page")를 통해 2를 가져올 수 있게 됩니다.
값이 없으면 default로 1을 설정하도록 코드를 짜줍시다.
from django.shortcuts import render
from . import models
# Create your views here.
def all_rooms(request):
# url에서 가져옴
page = int(request.GET.get("page", 1))
page_size = 10
limit = int(page_size * page)
offset = int(limit - page_size)
all_rooms = models.Room.objects.all()[offset:limit]
# rendering
return render(request, "rooms/home.html", context={"rooms": all_rooms})
이 경우 url이 정상적이면 잘 작동합니다. 그러나 http://127.0.0.1:8000/?page= 과 같이 값이 비어있게 되면 ""를 int로 바꿀 수 없으므로 오류를 냅니다. (default로 설정한 1은 page값이 아예 없을 때 작동하는 것입니다)
그러므로 오류가 나면 page에서 아예 1을 할당 해버리도록 합시다.
page = int(request.GET.get("page", 1) or 1)
pagination 구현
템플릿 내에서 range나 사칙연산이 불가능하므로 view단에서 모두 작성해서 context로 날려줍니다.
from math import ceil
from django.shortcuts import render
from . import models
# Create your views here.
def all_rooms(request):
page = int(request.GET.get("page", 1) or 1)
page_size = 10
limit = int(page_size * page)
offset = int(limit - page_size)
all_rooms = models.Room.objects.all()[offset:limit]
page_count = ceil(models.Room.objects.count() / page_size)
return render(
request,
"rooms/home.html",
{
"rooms": all_rooms,
"page": page,
"page_count": page_count,
"page_range": range(1, page_count + 1),
},
)
(https://docs.djangoproject.com/en/3.0/ref/templates/builtins/#add)
템플릿 내 사칙연산이 불가능하므로 template의 자체 필터를 사용해서 덧,뺄셈을 활용한 Next/Prev 버튼을 만들었습니다.
{% if page is not 1 %}
<a href="/?page={{page|add:"-1"}}">Previous</a>
{% endif %}
Page {{page}} of {{page_count}}
{% if page < page_count %}
<a href="/?page={{page|add:"1"}}">Next</a>
{% endif %}
{% for page in page_range %}
<a href="/?page={{page}}">{{page}}</a>
{% endfor %}
django-pagination
위와 같이 구현한 pagination을 django-pagination을 이용해서 쉽고 빠르게 구현할 수도 있다.
from django.core.paginator import Paginator
아래 코드에 적어놓은 속성 외에 다른 속성들은 위의 공식 문서를 참고합시다.
Paginator 객체 생성
# Paginator(object_list, per_page, orphans=0, allow_empty_first_page=True) 으로 작성
paginator = Paginator(room_list, per_page=10, orphans=5)
per_page는 한 page당 몇개를 출력할 것인지,
orphans는 몇 개 이상의 고아부터 출력할 것인지입니다.
이 개념이 이해가 잘 안 갈수 있는데 문서를 봅시다.
For example, with 23 items, per_page=10, and orphans=3, there will be two pages; the first page with 10 items and the second (and last) page with 13 items. orphans defaults to zero, which means pages are never combined and the last page may have one item.
Paginator 객체에서 이용할 수 있는 것은 크게, Paginator class와 Page class
# Page class입니다. get_page나 page 메서드를 사용합니다.
rooms = paginator.get_page(page)
# page에 해당하는 room_list 내의 객체, number(현재 몇 페이지), paginator 클래스
print(vars(rooms))
# Paginator class 입니다. Page class 내에 존재합니다
rooms.paginator
# room_list 내의 객체, per_page, orphans(per_page에 미달하는 고아들)
# count(총 몇 개), num_pages(총 몇 페이지)
print(vars(rooms.paginator))
여기서 잠깐, get_page와 page는 둘 다 Returns a Page object를 합니다. 그렇다면 차이가 뭘까요?
get_page는 기본적으로 handling out of range and invalid page numbers.page를 해줍니다. http://127.0.0.1:8000/?page=9999 같은 값을 줘도 알아서 마지막 페이지를 렌더하고 숫자가 아닌 값을 주면 첫번째 페이지로 돌려보냅니다.
page는 그런게 없는 대신 에러를 좀 더 세분화해서 (수작업으로) 핸들링할 수 있습니다. 예를 들어, 위와 같이 9999값을 주면 다른 페이지로 아예 이동시켜버린다던지, 각 에러마다 다른 처리를 해주는 것이 가능합니다. 좀 더 커스터마이징이 가능하다고 알아두면 됩니다.
이를 이용해 다음과 같이 작성할 수 있습니다.
from math import ceil
from django.shortcuts import render, redirect
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from . import models
# Create your views here.
def all_rooms(request):
# 우선 url에서 page를 가져옵니다. /?page=3 꼴. 없다면 1을 기본값으로 지정합시다.
page = request.GET.get("page", 1)
# 모든 Room 모델의 객체를 가져오도록 했습니다. 미디어 쿼리를 선언한 것만으로는 실행되지 않습니다.
room_list = models.Room.objects.all()
# 파지네이터 생성! 10개를 묶어 한 페이지로 설정하고, orphans 출력 기준은 5로 설정합니다.
paginator = Paginator(room_list, per_page=10, orphans=5)
try:
# page 클래스를 생성합니다. 실패하면 except로 넘어가게끔 합시다.
rooms = paginator.page(int(page))
return render(request, "rooms/home.html", {"rooms": rooms},)
except Exception:
# 에러시 redirect합니다.
return redirect("/")
<h4>
{% if rooms.has_previous %}
<a href="/?page={{rooms.previous_page_number}}">Previous</a>
{% endif %}
Page {{rooms.number}} of {{rooms.paginator.num_pages}}
{% if rooms.has_next %}
<a href="/?page={{rooms.next_page_number}}">Next</a>
{% endif %}
</h4>
{% for page in rooms.paginator.page_range %}
<a href="/?page={{page}}">{{page}}</a>
{% endfor %}
'Django, Flask > 🔫 Django' 카테고리의 다른 글
Path Converter와 namespace, name을 활용한 link (0) | 2020.06.16 |
---|---|
Class Based Views(CBV) 근데 generic-display를 곁들인 (0) | 2020.06.16 |
url과 view(FBV), django templates (0) | 2020.06.14 |
django-seed와 faker를 이용해 커스텀 명령어로 데이터 생성하기 (0) | 2020.05.13 |
custom django-admin commands (0) | 2020.05.13 |