5.3. SQL v. ORM

Bazy danych są niezbędnym składnikiem większości aplikacji. Poniżej zwięźle pokażemy, w jaki sposób z wykorzystaniem Pythona można je obsługiwać przy użyciu języka SQL, jak i systemów ORM na przykładzie rozwiązania Peewee.

Informacja

Niniejszy materiał koncentruje się na poglądowym wyeksponowaniu różnic w kodowaniu, komentarz ograniczono do minimum. Dokładne wyjaśnienia poszczególnych instrukcji znajdziesz w materiale SQL oraz Systemy ORM. W tym ostatnim omówiono również ORM SQLAlchemy.

5.3.1. Połączenie z bazą

Na początku pliku sqlraw.py umieszczamy kod, który importuje moduł do obsługi bazy SQLite3 i przygotowuje obiekt kursora, który posłuży nam do wydawania poleceń SQL:

Plik sqlraw.py. Kod nr
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#! /usr/bin/env python
# -*- coding: utf-8 -*-

import sqlite3

# utworzenie połączenia z bazą przechowywaną w pamięci RAM
con = sqlite3.connect(':memory:')

# dostęp do kolumn przez indeksy i przez nazwy
con.row_factory = sqlite3.Row

# utworzenie obiektu kursora
cur = con.cursor()

System ORM Peewee inicjujemy w pliku ormpw.py tworząc klasę bazową, która zapewni połączenie z bazą:

Plik ormpw.py. Kod nr
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#! /usr/bin/env python
# -*- coding: utf-8 -*-

import os
from peewee import *

if os.path.exists('test.db'):
    os.remove('test.db')
# tworzymy instancję bazy używanej przez modele
baza = SqliteDatabase('test.db')  # ':memory:'


# BazaModel to klasa bazowa dla klas Grupa i Uczen, które
# opisują rekordy tabel "grupa" i "uczen" oraz relacje między nimi
class BazaModel(Model):
    class Meta:
        database = baza

Informacja

Parametr :memory: powoduje utworzenie bazy danych w pamięci operacyjnej, która istnieje tylko w czasie wykonywania programu. Aby utworzyć trwałą bazę, zastąp omawiany parametr nazwę pliku, np. test.db.

5.3.2. Model bazy

Dane w bazie zorganizowane są w tabelach, połączonych najczęściej relacjami. Aby utworzyć tabele grupa i uczen powiązane relacją jeden-do-wielu, musimy wydać następujące polecenia SQL:

Plik sqlraw.py. Kod nr
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# tworzenie tabel
cur.executescript("""
    DROP TABLE IF EXISTS grupa;
    CREATE TABLE IF NOT EXISTS grupa (
        id INTEGER PRIMARY KEY ASC,
        nazwa varchar(250) NOT NULL,
        profil varchar(250) DEFAULT ''
    );
    DROP TABLE IF EXISTS uczen;
    CREATE TABLE IF NOT EXISTS uczen (
        id INTEGER PRIMARY KEY ASC,
        imie varchar(250) NOT NULL,
        nazwisko varchar(250) NOT NULL,
        grupa_id INTEGER NOT NULL,
        FOREIGN KEY(grupa_id) REFERENCES grupa(id)
    )""")

Wydawanie poleceń SQL-a wymaga koncentracji na poprawności użycia tego języka, systemy ORM izolują nas od takich szczegółów pozwalając skupić się na logice danych. Tworzymy więc klasy opisujące nasze obiekty, tj. grupy i uczniów. Na podstawie właściwości tych obiektów system ORM utworzy odpowiednie pola tabel. Konkretna grupa lub uczeń, czyli instancje klasy, reprezentować będą rekordy w tabelach.

Plik ormpw.py. Kod nr
20
21
22
23
24
25
26
27
28
29
30
31
32
class Grupa(BazaModel):
    nazwa = CharField(null=False)
    profil = CharField(default='')


class Uczen(BazaModel):
    imie = CharField(null=False)
    nazwisko = CharField(null=False)
    grupa = ForeignKeyField(Grupa, related_name='uczniowie')


baza.connect()  # nawiązujemy połączenie z bazą
baza.create_tables([Grupa, Uczen], True)  # tworzymy tabele

5.3.2.1. Ćwiczenie 1

Utwórz za pomocą tworzonych skryptów bazy w plikach o nazwach sqlraw.db oraz peewee.db. Następnie otwórz te bazy w interpreterze Sqlite i wykonaj podane niżej polecenia. Porównaj struktury utworzonych tabel.

sqlite> .tables
sqlite> .schema grupa
sqlite> .schema uczen

5.3.3. Wstawianie danych

Chcemy wstawić do naszych tabel dane dwóch grup oraz dwóch uczniów. Korzystając z języka SQL użyjemy następujących poleceń:

Plik sqlraw.py. Kod nr
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# wstawiamy dane uczniów
cur.execute('INSERT INTO grupa VALUES(NULL, ?, ?);', ('1A', 'matematyczny'))
cur.execute('INSERT INTO grupa VALUES(NULL, ?, ?);', ('1B', 'humanistyczny'))

# wykonujemy zapytanie SQL, które pobierze id grupy "1A" z tabeli "grupa".
cur.execute('SELECT id FROM grupa WHERE nazwa = ?', ('1A',))
grupa_id = cur.fetchone()[0]

# wstawiamy dane uczniów
cur.execute('INSERT INTO uczen VALUES(?,?,?,?)',
            (None, 'Tomasz', 'Nowak', grupa_id))
cur.execute('INSERT INTO uczen VALUES(?,?,?,?)',
            (None, 'Adam', 'Kowalski', grupa_id))

# zatwierdzamy zmiany w bazie
con.commit()

W systemie ORM pracujemy z instancjami inst_grupa i inst_uczen. Nadajemy wartości ich atrybutom i korzystamy z ich metod:

Plik ormpw.py. Kod nr
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# dodajemy dwie klasy, jeżeli tabela jest pusta
if Grupa.select().count() == 0:
    inst_grupa = Grupa(nazwa='1A', profil='matematyczny')
    inst_grupa.save()
    inst_grupa = Grupa(nazwa='1B', profil='humanistyczny')
    inst_grupa.save()

# tworzymy instancję klasy Grupa reprezentującą klasę "1A"
inst_grupa = Grupa.select().where(Grupa.nazwa == '1A').get()
# dodajemy uczniów
inst_uczen = Uczen(imie='Tomasz', nazwisko='Nowak', grupa=inst_grupa)
inst_uczen.save()
inst_uczen = Uczen(imie='Adam', nazwisko='Kowalski', grupa=inst_grupa)
inst_uczen.save()

5.3.4. Pobieranie danych

Pobieranie danych (czyli kwerenda) wymaga polecenia SELECT języka SQL. Aby wyświetlić dane wszystkich uczniów zapisane w bazie użyjemy kodu:

Plik sqlraw.py. Kod nr
50
51
52
53
54
55
56
57
58
59
60
61
62
63
def czytajdane():
    """Funkcja pobiera i wyświetla dane z bazy"""
    cur.execute(
        """
        SELECT uczen.id,imie,nazwisko,nazwa FROM uczen,grupa
        WHERE uczen.grupa_id=grupa.id
        """)
    uczniowie = cur.fetchall()
    for uczen in uczniowie:
        print(uczen['id'], uczen['imie'], uczen['nazwisko'], uczen['nazwa'])
    print()


czytajdane()

W systemie ORM korzystamy z metody select() instancji reprezentującej ucznia. Dostęp do danych przechowywanych w innych tabelach uzyskujemy dzięki wyrażeniom typu inst_uczen.grupa.nazwa, które generuje podzapytanie zwracające obiekt grupy przypisanej uczniowi.

Plik ormpw.py. Kod nr
50
51
52
53
54
55
56
57
def czytajdane():
    """Funkcja pobiera i wyświetla dane z bazy"""
    for uczen in Uczen.select():  # lub szybsze: Uczen.select().join(Grupa)
        print(uczen.id, uczen.imie, uczen.nazwisko, uczen.grupa.nazwa)
    print()


czytajdane()

Wskazówka

Ze względów wydajnościowych pobieranie danych z innych tabel możemy zasygnalizować już w głównej kwerendzie, używając metody join(), np.: Uczen.select().join(Grupa).

5.3.5. Modyfikacja i usuwanie danych

Edycja danych zapisanych już w bazie to kolejna częsta operacja. Jeżeli chcemy przepisać ucznia z grupy do grupy, w przypadku czystego SQL-a musimy pobrać identyfikator ucznia (uczen_id = cur.fetchone()[0]), identyfikator grupy (grupa_id = cur.fetchone()[0]) i użyć ich w klauzuli UPDATE. Usuwany rekord z kolei musimy wskazać w klauzuli WHERE.

Plik sqlraw.py. Kod nr
65
66
67
68
69
70
71
72
73
74
75
76
77
# przepisanie ucznia do innej grupy
cur.execute('SELECT id FROM uczen WHERE nazwisko="Nowak"')
uczen_id = cur.fetchone()[0]
cur.execute('SELECT id FROM grupa WHERE nazwa = ?', ('1B',))
grupa_id = cur.fetchone()[0]
cur.execute('UPDATE uczen SET grupa_id=? WHERE id=?', (grupa_id, uczen_id))
czytajdane()

# usunięcie ucznia o identyfikatorze 1
cur.execute('DELETE FROM uczen WHERE id=?', (1,))
czytajdane()

con.close()

W systemie ORM tworzymy instancję reprezentującą ucznia i zmieniamy jej właściwości (inst_uczen.grupa = Grupa.select().where(Grupa.nazwa == '1B').get()). Usuwając dane w przypadku systemu ORM, usuwamy instancję wskazanego obiektu:

Plik ormpw.py. Kod nr
59
60
61
62
63
64
65
66
67
68
69
70
# przepisanie ucznia do innej klasy
inst_uczen = Uczen.select().join(Grupa).where(Uczen.nazwisko == 'Nowak').get()
inst_uczen.grupa = Grupa.select().where(Grupa.nazwa == '1B').get()
inst_uczen.save()  # zapisanie zmian w bazie
czytajdane()

# usunięcie ucznia o identyfikatorze 1
inst_uczen = Uczen.select().where(Uczen.id == 1).get()
inst_uczen.delete_instance()
czytajdane()

baza.close()

Informacja

Po wykonaniu wszystkich założonych operacji na danych połączenie z bazą należy zamknąć, zwalniając w ten sposób zarezerwowane zasoby. W przypadku modułu sqlite3 wywołujemy polecenie con.close(), w Peewee baza.close().

5.3.6. Podsumowanie

Bazę danych można obsługiwać za pomocą języka SQL na niskim poziomie. Zyskujemy wtedy na szybkości działania, ale tracimy przejrzystość kodu, łatwość jego przeglądania i rozwijania. O ile w prostych zastosowaniach można to zaakceptować, o tyle w bardziej rozbudowanych projektach używa się systemów ORM, które pozwalają zarządzać danymi nie w formie tabel, pól i rekordów, ale w formie obiektów reprezentujących logicznie spójne dane. Takie podejście lepiej odpowiada obiektowemu wzorcowi projektowania aplikacji.

Dodatkową zaletą systemów ORM, nie do przecenienia, jest większa odporność na błędy i ewentualne ataki na dane w bazie.

Systemy ORM można łatwo integrować z programami desktopowymi i frameworkami przeznaczonymi do tworzenia aplikacji sieciowych. Wśród tych ostatnich znajdziemy również takie, w których system ORM jest podstawowym składnikiem, np. Django.

5.3.7. Zadania

  • Wykonaj scenariusz aplikacji Quiz ORM, aby zobaczyć przykład wykorzystania systemów ORM w aplikacjach internetowych.
  • Wykonaj scenariusz aplikacji internetowej Czat (cz. 1), zbudowanej z wykorzystaniem frameworku Django, korzystającego z własnego modelu ORM.

5.3.8. Źródła


Licencja Creative Commons Materiały Python 101 udostępniane przez Centrum Edukacji Obywatelskiej na licencji Creative Commons Uznanie autorstwa-Na tych samych warunkach 4.0 Międzynarodowa.

Utworzony:2022-05-22 o 19:52 w Sphinx 1.5.3
Autorzy:Patrz plik “Autorzy”