5.4. SQL vs 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 modułu sqlite3 i języka SQL, jak i systemów ORM na przykładzie frameworku 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 System ORM Peewee i System ORM SQLAlchemy.

5.4.1. Połączenie z bazą

Na początku pliku sql_raw.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 sql_raw.py. Kod nr
1import sqlite3
2
3# utworzenie połączenia z bazą przechowywaną w pamięci RAM
4con = sqlite3.connect(':memory:')
5# dostęp do kolumn przez indeksy i przez nazwy pól
6con.row_factory = sqlite3.Row
7# utworzenie obiektu kursora
8cur = con.cursor()
9

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

Plik orm_pw.py. Kod nr
 1from peewee import SqliteDatabase, Model
 2from peewee import CharField, ForeignKeyField
 3
 4# tworzymy instancję klasy Database do obsługi bazy przechowywanej w pamięci RAM
 5baza = SqliteDatabase(':memory:')
 6
 7# klasa bazowa dla modeli
 8class Base(Model):
 9    class Meta:
10        database = baza
11
12

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. baza.db.

5.4.2. Model bazy

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

Plik sql_raw.py. Kod nr
10# tworzenie tabel
11cur.executescript("""
12    DROP TABLE IF EXISTS klasa;
13    CREATE TABLE IF NOT EXISTS klasa (
14        id INTEGER PRIMARY KEY,
15        nazwa varchar(250) NOT NULL,
16        profil varchar(250) DEFAULT ''
17    );
18    DROP TABLE IF EXISTS uczen;
19    CREATE TABLE IF NOT EXISTS uczen (
20        id INTEGER PRIMARY KEY,
21        imie varchar(250) NOT NULL,
22        nazwisko varchar(250) NOT NULL,
23        klasa_id INTEGER NOT NULL,
24        FOREIGN KEY(klasa_id) REFERENCES klasa(id)
25    )""")
26

Wydawanie poleceń SQL-a wymaga znajomości składni tego języka. Systemy ORM, chociaż pozwalają na wykonywanie zapytań SQL, oferują manipulowanie obiektami, a nie tabelami. W powyższym kodzie zamiast tabel definiujemy więc modele, tj. klasy reprezentujące klasy i uczniów. Na podstawie właściwości modeli system ORM utworzy tabele i odpowiednie pola, tj. wygeneruje i wykona odpowiednie zapytania SQL.

Plik orm_pw.py. Kod nr
13# klasy Klasa i Uczen opisują rekordy tabel "klasa" i "uczen" oraz relacje między nimi
14class Klasa(Base):
15    nazwa = CharField(null=False)
16    profil = CharField(default='')
17
18
19class Uczen(Base):
20    imie = CharField(null=False)
21    nazwisko = CharField(null=False)
22    klasa = ForeignKeyField(Klasa, related_name='uczniowie')
23
24
25baza.connect()  # nawiązujemy połączenie z bazą
26baza.create_tables([Klasa, Uczen])  # tworzymy tabele
27

5.4.2.1. Ćwiczenie 1

Utwórz za pomocą powyższych skryptów bazy w plikach o nazwach sql_raw.db oraz orm_pw.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
../../_images/sql_raw_db.png
../../_images/orm_pw_db.png

5.4.3. Dodawanie danych

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

Plik sql_raw.py. Kod nr
27# dodajemy dwie klasy
28cur.execute('INSERT INTO klasa VALUES(NULL,?,?)', ('1A', 'matematyczny'))
29klasa1_id = cur.lastrowid
30cur.execute('INSERT INTO klasa VALUES(NULL,?,?)', ('1B', 'humanistyczny'))
31klasa2_id = cur.lastrowid
32
33# lista uczniów, których dane zapisane są w krotkach
34uczniowie = [
35    ('Tomasz', 'Nowak', klasa1_id),
36    ('Jan', 'Kos', klasa2_id),
37    ('Piotr', 'Kowalski', klasa2_id)
38]
39# dodajemy dane wielu uczniów
40cur.executemany('INSERT INTO uczen VALUES(NULL,?,?,?)', uczniowie)
41con.commit()
42

Metoda kursora execute() pozwala wykonać pojedyncze zapytanie SQL. Metoda executemany() wykonuje jedno zapytanie dla danych podanych w liście krotek. Dane zapisane krotkach odpowiadają rekordom w tabeli. Znaki zapytania (zamienniki) w poleceniach SQL są zastępowane przez kolejne parametry przekazywane w krotkach.

W systemie ORM tworzymy obiekty (instancje) na podstawie klas modelu. Właściwości obiektów przekazujemy metodzie create(), która od razu tworzy rekord w bazie, lub konstruktorowi, po którym wywołujemy metodę save().

Plik orm_pw.py. Kod nr
28# dodajemy dwie klasy
29klasa1 = Klasa.create(nazwa='1A', profil='matematyczny')
30klasa2 = Klasa(nazwa='1B', profil='humanistyczny')
31klasa2.save()
32
33# lista uczniów, których dane zapisane są w słownikach
34uczniowie = [
35    {'imie': 'Tomasz', 'nazwisko': 'Nowak', 'klasa': klasa1},
36    {'imie': 'Jan', 'nazwisko': 'Kos', 'klasa': klasa2},
37    {'imie': 'Piotr', 'nazwisko': 'Kowalski', 'klasa': klasa2}
38]
39# dodajemy dane wielu uczniów
40Uczen.insert_many(uczniowie).execute()
41

Do dawania wielu rekordów używamy metody insert_many() modelu, która przyjmuje jako argument listę słowników opisujących właściwości kolejnych obiektów.

5.4.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 sql_raw.py. Kod nr
43def wypisz_listę_uczniow():
44    """ Odczytujemy i wypisujemy dane uczniów, w tym klasę"""
45    cur.execute("SELECT count(*) FROM uczen")
46    if (cur.fetchone()[0]):
47        print('Uczniowie:')
48        cur.execute(
49            """
50            SELECT uczen.id, imie, nazwisko, nazwa FROM uczen
51            INNER JOIN klasa
52            WHERE uczen.klasa_id=klasa.id
53            """)
54        uczniowie = cur.fetchall()
55        for uczen in uczniowie:
56            print(uczen['id'], uczen['imie'], uczen['nazwisko'], uczen['nazwa'])
57        print()
58
59wypisz_listę_uczniow()
60

Funkcja wypisz_liste_uczniow() na początku wykonuje zapytanie o liczbę rekordów w tabeli uczen. Jeżeli w tabeli są jakieś rekordy (if (cur.fetchone()[0]):), pobieramy informacje o wszystkich uczniach wykonując zapytanie SQL i wypisujemy je.

W systemie ORM korzystamy z metod modelu select().count(), aby sprawdzić, czy w tabeli uczen są jakieś dane. Jeżeli tak, wykorzystujemy metody select().join(), aby pobrać dane uczniów i klas, do których należą. W pętli for odczytujemy zwrócone obiekty i wypisujemy ich właściwości korzystając z notacji z kropką:

Plik orm_pw.py. Kod nr
42def wypisz_listę_uczniow():
43    """ Odczytujemy i wypisujemy dane uczniów, w tym klasę"""
44    if Uczen.select().count():
45        print('Uczniowie:')
46        uczniowie = Uczen.select().join(Klasa)
47        for uczen in uczniowie:
48            print(uczen.id, uczen.imie, uczen.nazwisko, uczen.klasa.nazwa)
49        print()
50    else:
51        print('Brak uczniów w bazie!')
52
53wypisz_listę_uczniow()
54

5.4.5. Modyfikacja danych

Edycja danych zapisanych już w bazie to kolejna częsta operacja. Jeżeli chcemy przepisać ucznia do innej klasy, w przypadku SQL-a musimy pobrać identyfikator ucznia. To wymaga wykonania zapytania, np. SELECT id FROM uczen WHERE nazwisko="Nowak" i pobrania wartości pierwszego zwróconego obiektu: uczen_id = cur.fetchone()[0].

Podobnie uzyskujemy identyfikator klasy.

Na koniec używamy zapytania aktualizującego: UPDATE uczen SET klasa_id=? WHERE id=?, któremu podajemy w krotce identyfikator nowej klasy i ucznia.

Plik sql_raw.py. Kod nr
61# przepisanie wybranego ucznia do innej klasy
62cur.execute('SELECT id FROM uczen WHERE nazwisko=?', ('Nowak',))
63uczen_id = cur.fetchone()[0]
64cur.execute('SELECT id FROM klasa WHERE nazwa=?', ('1B',))
65klasa_id = cur.fetchone()[0]
66cur.execute('UPDATE uczen SET klasa_id=? WHERE id=?', (klasa_id, uczen_id))
67wypisz_listę_uczniow()
68

W systemie ORM tworzymy obiekty reprezentujące ucznia i nową klasę. Następnie zmieniamy klasę ucznia, przypisując odpowiadającej właściwości obiekt nowej klasy (uczen.klasa = nowa_klasa).

Plik orm_pw.py. Kod nr
55# zmiana klasy ucznia o identyfikatorze 2
56uczen = Uczen.select().join(Klasa).where(Uczen.id == 2).get()
57nowa_klasa = Klasa.select().where(Klasa.nazwa == '1A').get()
58print('Zmieniam klasę ucznia:', uczen.imie, uczen.nazwisko, uczen.klasa.nazwa)
59uczen.klasa = nowa_klasa
60uczen.save()  # zapisanie zmian w bazie
61wypisz_listę_uczniow()
62

5.4.6. Usuwanie danych

Usuwanie danych w języku SQL wymaga użycia klauzuli DELETE, która na podstawie podanych kryteriów wybiera i usuwa wybrany z tabeli rekord, np.:

Plik sql_raw.py. Kod nr
69# usunięcie ucznia o identyfikatorze 1
70cur.execute('DELETE FROM uczen WHERE id=?', (1,))
71wypisz_listę_uczniow()
72
73con.close()

W systemach ORM zazwyczaj na początku odczytujemy interesujący nas obiekt za pomocą metody select(), a następnie usuwamy za pomocą metody delete_instance().

Plik orm_pw.py. Kod nr
63# usunięcie ucznia o identyfikatorze 1
64uczen = Uczen.select().where(Uczen.id == 1).get()
65print('Usuwam ucznia:', uczen.id, uczen.imie, uczen.nazwisko)
66uczen.delete_instance()
67wypisz_listę_uczniow()
68
69baza.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.4.7. Podsumowanie

Bazę danych można obsługiwać za pomocą języka SQL na niskim poziomie. Zyskujemy wtedy na szybkości działania, ale wymaga to znajomości SQL-a, a kod może być mniej czytelny, a jego rozwijanie trudniejsze. W bardziej rozbudowanych projektach zazwyczaj używamy systemów ORM, które pozwalają zarządzać danymi nie w formie tabel, pól i rekordów, ale w postaci obiektów reprezentujących modele, tj. klasy. Takie podejście odpowiada obiektowemu wzorcowi projektowania aplikacji.

Dodatkową zaletą systemów ORM 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.4.8. Zadania

  1. Wykonaj scenariusze aplikacji System ORM Peewee i/lub System ORM SQLAlchemy, aby poznać przykłady użycia systemów ORM.

  2. Wykonaj scenariusz aplikacji internetowej Czat (cz. 1), zbudowanej z użyciem frameworku Django, korzystającego z własnego modelu ORM.


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:

2026-05-30 o 19:12 w Sphinx 7.3.7

Autorzy:

Robert Bednarz