kleindan.dev

Wykorzystanie baz danych w Django: podstawy

Tym razem w naszej lekcji cofniemy się o jeden krok, zanim wykonamy dwa kolejne. Kiedy skończyliśmy ostatnią lekcję, oba formularze w naszej aplikacji, formularz do głosowania oraz do tworzenia nowych ankiet były obsługiwane przez klasy pomocnicze Django.

Wróćmy na chwilę do koncepcji obsługi formularza bez wykorzystania klas pomocniczych Django i spróbujmy dodać do naszej aplikacji obsługę połączenia z bazą danych.

Skąd wziąć bazę danych i dlaczego już ją mam?

Po pierwsze, jeżeli ktoś czytał ostrzeżenia podczas każdego uruchamiania aplikacji, to prawdopodobnie podejrzewa, że Django już zakładało jakieś połączenie z bazą danych. To fakt, framework Django zakłada, że nasza aplikacja będzie korzystała z bazy danych. Domyślnie utworzony projekt zawiera aplikację admin, która już sama w sobie zawiera pewne typy danych, które autorzy Django uznali na tyle powszechne we wszystkich aplikacjach internetowych, że z góry zakładają, że programista będzie chciał z nich korzystać.

Co do wyboru domyślnego silnika bazy danych, to Django też ma konrytną, domyślną propozycję dla użytkowników, jest to mianowicie darmowy i prosty SQLite. Należy zaznaczyć, że SQLite przez lata nie był traktowany poważnie jako baza danych, która może być używana “na produkcji”, ale po latach rozwoju SQLite okazuje się zupełnie wystarczającym rozwiązaniem dla wielu projektów. Oczywiście Django wspiera wiele innych silników bazodanowych, ale na chwilę obecną wystarczy nam wybór domyślny.

Kontakt z bazą (danych)

Do kontaktu z bazą danych Django używa tzw. klas modeli nazwanych tak ze względu na zastosowaną architekturę typu Model-View-Controller. Zaczniemy od stworzenia prostego zestwu dwóch klas opisujących dane, na których działa nasza aplikacja: ankiet oraz przypisanych do nich zestawów odpowiedzi wraz z ilością oddanych na te odpowiedzi głosów.
Wyedytujmy w tym celu nieruszany dotychczas plik models.py:

> polls/models.my
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from django.db import models

# Create your models here.
class Poll(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField("date published")

    def __str__(self):
        return self.question_text
    
class Choice(models.Model):
    question = models.ForeignKey(Poll, on_delete=models.CASCADE)
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)

    def __str__(self):
        return self.choice_text

Jeżeli pracowaliście wcześniej z bazami danych, to taki opis może być dla Was naturalny, ale przejdźmy przez niego pokolei:

Po dodaniu tych modeli możemy użyć skryptu manage.py do stworzenia dla nas skryptów SQL, które stworzą w bazie danych odpowiednie tabele do przechowywania naszcy danych, a następnie do uruchomienia tych skryptów. Zrobimy to używając komend:

$ ./manage.py makemigrations
$ ./manage.py migrate

Po uruchomieniu tych skryptów zasadniczo nic się w naszej aplikacji nie zmieni, ale możemy zacząć już pracować z bazą danyc. Zacznijmy od wykorzystania panelu administracyjnego Django.

Integracja modeli z wbudowaną aplikacją administracyjną

Zacznijmy od dodania naszego modelu do strony admin.

> polls/admin.my
from django.contrib import admin

from .models import Poll

admin.site.register(Poll)

Teraz w terminalu stwórz nowe konto administratorskie dla swojej aplikacji, a następnie wejdź na stronę na ścieżkę admin/ i zaloguj się używając danych stworzonego przed chwilą konta użytkownika.

$ ./manage.py createsuperuser

Teraz nasza strona powinna pozwolić na tworzenie nowych ankiet, przeglądanie tych już obecnych w bazie danych i usuwanie ich. Oczywiście na początku lista ta będzie pusta.

Stwórz przynajmniej dwie ankiety, a potem zabierzemy się za wydobycie danych z bazy w naszej aplikacji.

Czytanie z bazy danych

Zastąp swój obecny widok index następującym kodem:

> polls/views.my

from .models import Poll

def index(request):
    context = {
        "latest_polls": Poll.objects.order_by("-pub_date")[:5]
    }
    return render(request, "polls/index.html", context)

Dużo się dzieje w tej jednej linijce kodu! Czyli do danych odpowiadających klasie Polls mogę się dostać przez pole Polls.objects. Następnie sortuję te dane po dacie publikacji (od najnowszych), a potem wybieram pierwsze pięć z nich.

Skoro mamy dane, to jeszcze trzeba je jakoś obsłużyć w szablonie HTML.
Zastąp obecną zawartość pliku następującą treścią:

> polls/templates/polls.html
{% extends "polls/base.html" %}

{% block content %}
    Recent polls:
    <ul>
    {% for poll in latest_polls %}
        <li><a href="{% url 'polls:poll' poll.id %}">{{ poll.question_text }}</a></li>
    {% endfor %}
    </ul>
{% endblock %}

Zwróć uwagę, że tworząc url do ankiety posłużyliśmy się zmienną poll.id. Nasza ankieta nie ma już krótkiej nazwy, do której możemy się odnieść, ale w zamian za to Django stworzyło dla nas unikatowy identyfikator, po którym możemy odszukać ankietę. Poszukaj tego w identyfikatora używając panelu administracyjnego Django.

Strefy czasowe w Django

Jeżeli daty lub godziny nie zgadzają się do końca z tym, czego się spodziewasz, upewnij się, że zarówno Django jak i Twoja maszyna wirtualna korzystają z tej samej strefy czasowej.

Ustawienie strefy czasowej na systemie Ubuntu Linux:

$ sudo timedatectl set-timezone Europe/Warsaw

Ustawienie tej samej strefy w aplikacji Django:

> mysite/settings.py
# ...

TIME_ZONE = 'Europe/Warsaw'

# ...

Wyświetlanie formularzy do głosowania w ankietech

Aby wyświetlić naszą ankietę i umożliwić głosowanie w niej, musimy znów dostosować odpowiednio widok, wykorzystywany przez niego szablon i upewnić się, że aplikacja poprawnie obsłuży ścieżki. Zacznijmy od ostatniego punktu.

Ponieważ nasze ankiety nie mają obecnie nazw, ale mają identyfikatory w bazie danych, zmienimy nazwę widoku i jego argumentu na bardziej adekwatną:

> polls/urls.my
app_name = "polls"
urlpatterns = [
    path('', views.index, name="index"),
    path('<int:poll_id>/', views.by_id, name="details"),
    path('vote/<str:poll_name>/', views.vote, name="vote"),
    path('results/<str:poll_name>/', views.results, name="results"),
]

Następnie zmodyfikujemy sam widok:

> polls/views.my
# ...

def by_id(request, poll_id):
    context = {}
    retcode = 200
    try:
        context['poll'] = Poll.objects.get(pk=poll_id)
    except Poll.DoesNotExist:
        retcode = 404
    return render(request, "polls/poll.html", context, status=retcode)

Poll.objects.get(pk=poll_id) jest tu kluczowe. pk to skrót od primary key, a metoda get zakłada, że w bazie danych będzie jeden i tylko jeden obiekt spełniający warunki wyszukiwania. Skoro szukamy ankiety o konkretnym, unikalnym identyfikatorze, to powinno tak właśnie być.

A na sam koniec wyświtlimy dane używając naszego szablonu HTML:

> polls/templates/polls/poll.html
{% extends "polls/base.html" %}

{% block content %}
    {% if poll %}
        <form action="{% url 'polls:vote' poll.id%}" method="post">
        {% csrf_token %}
        <fieldset>
            <legend><h3>{{ poll.question_text }}</h3></legend>
            {% for choice in poll.choice_set.all %}
                <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ forloop.counter0 }}">
                <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br>
            {% endfor %}
        </fieldset>
        <input type="submit" value="Vote">
        </form>
    {% else %}
        <p>No such poll!</p>
    {% endif %}
{% endblock %}

Dodawanie danych z konsoli Pythona

Co z tego, że mamy możlwiość wyświetlenia ankiet, skoro nasze ankiety nie mają żadnych przypisanych opcji odpowiedzi?

Moglibyśmy stworzyć znów interface do tego celu w panelu administracyjnym, ale jest inna, potencjalnie szybsza opcja. Używając wiersza poleceń możemy użyć skryptu manage.py do stworzenia dla nas Pythonowego shella, w którym będziemy mogli ręcznie modyfikować bazę danych korzystając z istniejących klas naszej aplikacji.

$ ./manage.py shell
>>> from polls.models import Poll
>>> q = Poll.objects.get(pk=1)
>>> q
<Poll: What is the best ice-cream flavour?>
>>> q.choice_set.create(choice_text="chocolate")
>>> q.choice_set.create(choice_text="vanilla")

W mojej aplikacji zacząłem od stworzenia pytania o najlepszy smak lodów. Django automatycznie nadał tej ankiecie id z numerem 1. W powyższej sesji, stworzyłem następnie dwie możliwe odpowiedzi. Po wejściu na stronę ankiety, mogłem zobaczyć czy faktycznie pojawiły się one w bazie.

Przywrócenie funkcjonalności głosowania

O ile udało nam się wyświetlić poprawnie dane z bazy danych, o tyle samo głosowanie oraz walidacja danych od użytkownika znów nie działa. Wróćmy do koncepcji użycia klas pomocniczych Django do obsługi formularza za nas: Zacznijmy od stworzenia samej klasy formularza. Utwórz nowy plik forms.py

> polls/forms.my
from django import forms

class VoteForm(forms.Form):
    choice = forms.ChoiceField(
        widget=forms.RadioSelect,
        required=True,
    )

    def __init__(self, poll, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['choice'].choices = [(choice.id, choice.choice_text) for choice in poll.choice_set.all()]
        self.fields['choice'].label = poll.question_text

W linii 11 używamy dość skomplikowanego wyrażenia:

[(choice.id, choice.choice_text) for choice in poll.choice_set.all()]

W tym miejscu Django oczekuje listy dostępnych wartości w formie [(wartosc, etykieta), (wartosc, etykieta), ...] więc korzystając z konstrukcji list comprehensions przygotowujemy listę w dokładnie takim formacie.

Skoro mamy już formularz, to użyjmy go w naszym widoku wyświetlającym ankietę:

> polls/views.my
from .forms import VoteForm

# ...

def by_id(request, poll_id):
    context = {}
    retcode = 200
    try:
        poll = Poll.objects.get(pk=poll_id)
        context['poll'] = poll
        context['vote_form'] = VoteForm(poll)
    except Poll.DoesNotExist:
        retcode = 404
    return render(request, "polls/poll.html", context, status=retcode)

Oraz użyjmy go w naszym szablonie HTML:

> polls/templates/polls/poll.html
{% extends "polls/base.html" %}

{% block content %}
    {% if poll and vote_form %}
        <form action="{% url 'polls:vote' poll.id%}" method="post">
        {% csrf_token %}
        {{ vote_form }}
        <input type="submit" value="Vote">
        </form>
    {% else %}
        <p>No such poll!</p>
    {% endif %}
{% endblock %}

Jeżeli szablon nadal poprawnie się wyświetla, to możemy przejść do obsłużenia głosowania w widoku vote:

> polls/views.my
from django.db.models import Sum

from .models import Poll, Choice

def vote(request, poll_id):
    if request.method == 'GET':
        return HttpResponseRedirect(reverse("polls:details", args=[poll_id]))
    try:
        poll = Poll.objects.get(pk=poll_id)
        vote_form = VoteForm(poll, data=request.POST)
        if vote_form.is_valid():
            selected_choice = Choice.objects.get(pk = vote_form.cleaned_data['choice'])
            selected_choice.votes += 1
            selected_choice.save()
            return HttpResponseRedirect(reverse("polls:results", args=[poll_id]))
        else:
            context = {
                'poll': poll,
                'vote_form': vote_form
            }
            return render(request, "polls/poll.html", context)
            
    except Poll.DoesNotExist:
        return render(request, "polls/poll.html", status=404)

Jak widać obsługa nie zmieniła się znacząco od poprzedniej implementacji. Tworzymy wypełniony formularz na bazie danych przesłanych przez użytkownika w request.POST, a następnie jeżeli dane są prawidłowe, odnotowujemy oddany głos i zapisujemy informację w bazie danych.

Wyświetlanie wyników będzie działało podobnie jak ostatnio, oto jak będzie wyglądał nasz kod:

> polls/views.my
def results(request, poll_id):
    context = {}
    retcode = 200
    try:
        poll = Poll.objects.get(pk=poll_id)
        context['poll'] = poll
        votes = poll.choice_set.aggregate(total=Sum('votes'))
        context['total_votes'] = max(1, votes['total'])
    except Poll.DoesNotExist:
        retcode = 404
    return render(request, "polls/results.html", context, status=retcode)

W tym wypadku wykorzystamy magię bazy danych aby to ona za nas podliczyła, ile łącznie głosów zostało oddanych w ankiecie.

Pozostaje nam jeszcze przygotować szablon HTML do wyświetlenia wyników:

> polls/templates/polls/results.html
{% extends "polls/base.html" %}

{% block content %}
    {% if poll %}
        <p>{{ poll.question_text }}</p>
        <ul>
            {% for choice in poll.choice_set.all %}
            <li>
                <span class="poll-option">{{ choice.choice_text }}: {{choice.votes}} vote(s)</span><br>
                <span class="poll-votes" style="--votes: {{choice.votes}}; --total-votes: {{total_votes}};"></span>
            </li>
            {% endfor %}
        </ul>
    {% else %}
        <p>No such poll!</p>
    {% endif %}
{% endblock %}

Tym sposobem nasza aplikacja powinna być w stanie obsłużyć znów głosowanie w ankietach.

Wyzwanie: sam dodaj opcję tworzenia nowych ankiet

W poprzedniej lekcji dodaliśmy też możliwość tworzenia własnych ankiet.
Używając tamtych instrukcji i wszystkiego, czego się nauczyłeś w czasie przerabiania tego materiału, spróbuj odtworzyć tamtą funkcjonalność z wykorzystaniem bazy danych. Powodzenia!

Gdyby zadanie jednak okazało się za trudne, możesz zawsze sprawdzić rozwiązanie w repozytorium z kodem z lekcji.