본문 바로가기
Django

첫 번째 Django 앱 만들기 (Part 4: Forms and generic)

by AlbertIm 2024. 8. 19.

시작

Part3을 이어서 간단한 설문조사(Polls) 앱을 만드는 과정을 통해 Django의 Froms와 generic을 알아보려고 합니다. 본 포스트에서는 macOS와 IntelliJ IDEA Ultimate을 사용합니다.

본문

F() expressions

F() 객체는 모델 필드의 값, 모델 필드의 변환된 값, 또는 주석이 달린 열을 나타내는 객체입니다. 이를 사용하면 모델 필드 값을 참조하고 데이터베이스에서 이 값을 파이썬 메모리로 가져오지 않고도 데이터베이스 작업을 수행할 수 있습니다.

대신 Django는 F() 객체를 사용하여 데이터베이스 수준에서 필요한 작업을 설명하는 SQL 표현식을 생성합니다.

 

예를 들어

reporter = Reporters.objects.get(name="Albert") 
reporter.stories_filed += 1 
reporter.save()

 

위 코드에서는 reporter.stories_filed의 값을 데이터베이스에서 메모리로 가져와서 파이썬 연산자를 사용하여 수정한 후, 다시 데이터베이스에 저장했습니다.

 

다음과 같이 할 수도 있습니다.

from django.db.models import F

reporter = Reporters.objects.get(name="Albert")
reporter.stories_filed = F("stories_files") + 1
reporter.save()

 

reporter.stories_filed = F('stories_filed') + 1은 일반적인 파이썬 할당처럼 보이지만 사실 이는 데이터베이스에서의 작업을 설명하는 SQL 구조입니다.

 

reporter.stories_filed의 값이 무엇이든 파이썬은 이를 알지 못합니다. 이 값은 전적으로 데이터베이스에서 처리됩니다. 파이썬은 Django의 F() 클래스를 통해 필드를 참조하고 작업을 설명하는 SQL 구문만 생성합니다.

 

저장된 새로운 값에 접근하려면 객체를 다시 로드해야 합니다.

reporter = Reporters.objects.get(pk=reporter.pk)
# or
reporter.refresh_from_db()

 

단일 인스턴스에 대한 작업뿐만 아니라 F()는 객체 인스턴스의 QuerySet에 대해서도 update()와 함께 사용할 수 있습니다. 이렇게 하면 get()save()로 사용하는 두 개의 쿼리를 하나로 줄일 수 있습니다.

reporter = Reporter.objects.filter(name="Albert")
reporter.update(stories_filed=F("stories_filed")+1)

 

또한 update()를 사용하여 여러 객체의 필드 값을 증가시킬 수 있습니다. 이는 데이터베이스에서 파이썬으로 모든 데이터를 가져와 루프를 돌며 각 객체의 필드 값을 증가시키고 다시 저장하는 것보다 훨씬 더 빠를 수 있습니다.

Reporter.objects.update(stories_filed=F("stories_filed") + 1)

 

따라서 F()는 다음과 같은 성능상의 이점을 제공합니다:

  • 파이썬이 아닌 데이터베이스에서 작업을 수행하도록 합니다.
  • 일부 작업에 필요한 쿼리 수를 줄입니다.

간단한 폼 작성

polls/deltail.html 템플릿을 수정합니다.

  • {% for choice in question.choice_set.all %} : question.choice_set.all 에 대해 for문을 돌립니다.
  • {{ forloop.counter }}: for가 반복을 한 횟수를 가져옵니다.
  • {{ csrf_token }}: Django에서 CSRF(Cross Site Request Forgeries)를 방지하기 위하 POST를 보낼 때 이 템플릿 태그를 사용합니다.

 

<form action="{% url 'polls:vote' question.id %}" method="post">  
    {% csrf_token %}  
    <fieldset>  
        <legend><h1>{{ question.question_text }}</h1></legend>  
        {% if error_message %}  
            <p><strong>{{ error_message }}</strong></p>  
        {% endif %}  
        {% for choice in question.choice_set.all %}  
            <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}">  
            <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label>  
            <br>        
        {% endfor %}  
    </fieldset>  
    <input type="submit" value="Vote">  
</form>

 

polls/views.pyvote 뷰 함수를 수정합니다.

  • request.POST는 제출된 데이터를 키 이름으로 접근할 수 있게 해주는 dictionary와 유사한 객체입니다. 위 코드에서는 POST 호출에서 choice의 값을 가져옵니다. request.POST의 값은 항상 문자열입니다. Django는 GET 데이터에 액세스하기 위한 request.GET도 제공합니다.
  • request.POST['choice']는 POST 데이터에 choice가 제공되지 않았을 경우 KeyError를 발생시킵니다.
  • F("votes") + 1은 데이터베이스에서 투표수를 1만큼 증가시키는 코드입니다.
  • choice의 투표 수를 증가시킨 후, 코드는 HttpResponse가 아니라 HttpResponseRedirect를 반환합니다. HttpResponseRedirect는 사용자에게 리디렉션 할 URL을 인수로 받습니다.
  • HttpResponseRedirect 생성자에서 reverse() 함수를 사용하고 있습니다. 이 함수는 뷰 함수에서 URL을 하드코딩할 필요 없이 URL을 동적으로 생성할 수 있도록 도와줍니다.

 

def vote(request, question_id):  
    question = get_object_or_404(Question, pk=question_id)  
    try:  
        selected_choice = question.choice_set.get(pk=request.POST["choice"])  
    except (KeyError, Choice.DoesNotExist):  
        return render(  
                request,  
                "polls/detail.html",  
                {  
                    "question": question,  
                    "error_message": "You didn't select a choice.",  
                },  
        )  
    else:  
        selected_choice.votes = F("votes") + 1  
        selected_choice.save()  
        return HttpResponseRedirect(  
            reverse("polls:results", args=(question.id,)))

 

polls/views.pyresults뷰 함수를 수정합니다.

def results(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, "polls/results.html", {"question": question})

 

polls/results.html 템플릿을 생성합니다.

<h1>{{ question.question_text }}</h1>

<ul>
{% for choice in question.choice_set.all %}
    <li>{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li>
{% endfor %}
</ul>

<a href="{% url 'polls:detail' question.id %}">Vote again?</a>

 

이제 브라우저에서 /polls/3로 이동하여 질문에 투표할 때마다 업데이트되는 결과 페이지가 표시됩니다.

Generic views 사용: Less code is better

Django에서는 URL에 전달된 매개변수에 따라 데이터베이스에서 데이터를 가져오고 템플릿을 로드하여 렌더링 된 템플릿을 반환하는 매우 일반적인 작업에 대한 shortcut를 제공합니다.

 

Generic views는 일반적인 패턴을 추상화하여 애플리케이션을 작성하기 위해 Python 코드를 전혀 작성할 필요가 없을 정도로 단순화합니다.

 

polls/urls.py를 수정합니다.

from django.urls import path

from . import views

app_name = "polls"
urlpatterns = [
    path("", views.IndexView.as_view(), name="index"),
    path("<int:pk>/", views.DetailView.as_view(), name="detail"),
    path("<int:pk>/results/", views.ResultsView.as_view(), name="results"),
    path("<int:question_id>/vote/", views.vote, name="vote"),
]

 

<question_id><pk>로 변경했습니다. 이는 Detail()results() 뷰를 대체하기 위해 DetailView generic view를 사용할 것이며 URL에서 캠쳐된 기본 키 값이 pk로 설정하는 것이 필요합니다.

 

polls/views.py를 수정합니다.

from django.db.models import F  
from django.http import HttpResponseRedirect  
from django.shortcuts import get_object_or_404  
from django.shortcuts import render  
from django.urls import reverse  
from django.views import generic  

from .models import Choice  
from .models import Question  


class IndexView(generic.ListView):  
    template_name = "polls/index.html"  
    context_object_name = "latest_question_list"  

    def get_queryset(self):  
        """Return the last five published questions."""  
        return Question.objects.order_by("-pub_date")[:5]  


class DetailView(generic.DetailView):  
    model = Question  
    template_name = "polls/detail.html"  


class ResultsView(generic.DetailView):  
    model = Question  
    template_name = "polls/results.html"  


def vote(request, question_id):
# 유지

 

각 generic view는 어떤 모델에 대해 작업할지를 모델 속성(DetailViewmodel 속성) 또는 get_queryset() 메서드(IndexViewget_queryset)를 제공하여 알려줍니다.

 

기본적으로 DetailView generic view는 <앱 이름>/<모델 이름>_detail.html이라는 템플릿을 사용합니다. template_name 속성을 사용하여 Django에게 자동 생성된 템플릿 이름 대신 특정 템플릿 이름을 사용하도록 지정할 수 있습니다.

 

마찬가지로 ListView generic view는 기본적으로 <앱 이름>/<모델 이름>_list.html이라는 템플릿을 사용합니다. template_name 속성을 사용하여 원하는 템플릿을 지정할 수 있습니다.

 

DetailView에서는 <모델 이름> 컨텍스트 변수를 자동으로 제공하고 ListView에서는 <모델 이름>_list 컨텍스트 변수를 제공합니다. context_object_name 속성을 오버라이드하여 다른 변수 이름을 지정할 수 있습니다

마치며

이번 Part4에서는 Django의 generic views를 활용하여 코드를 간소화하고 generic views의 기본 개념을 익혔습니다. generic views를 사용하니 코드가 확실히 간결해지고 코드 작성 속도가 빨라졌으며 유지보수성도 향상된 것을 확인할 수 있었습니다. F() 표현식도 다루었으며, 이를 통해 데이터베이스에서 직접 값을 업데이트하는 방법을 정리했습니다. Generic views에 대해 좀 더 상세히 다룬 포스트를 별도로 작성할 예정입니다.

참고자료

댓글