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:
1#! /usr/bin/env python
2# -*- coding: utf-8 -*-
3
4import sqlite3
5
6# utworzenie połączenia z bazą przechowywaną w pamięci RAM
7con = sqlite3.connect(':memory:')
8
9# dostęp do kolumn przez indeksy i przez nazwy
10con.row_factory = sqlite3.Row
11
12# utworzenie obiektu kursora
13cur = con.cursor()
System ORM Peewee inicjujemy w pliku ormpw.py
tworząc klasę bazową, która zapewni połączenie z bazą:
1#! /usr/bin/env python
2# -*- coding: utf-8 -*-
3
4import os
5from peewee import *
6
7if os.path.exists('test.db'):
8 os.remove('test.db')
9# tworzymy instancję bazy używanej przez modele
10baza = SqliteDatabase('test.db') # ':memory:'
11
12
13# BazaModel to klasa bazowa dla klas Grupa i Uczen, które
14# opisują rekordy tabel "grupa" i "uczen" oraz relacje między nimi
15class BazaModel(Model):
16 class Meta:
17 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:
15# tworzenie tabel
16cur.executescript("""
17 DROP TABLE IF EXISTS grupa;
18 CREATE TABLE IF NOT EXISTS grupa (
19 id INTEGER PRIMARY KEY ASC,
20 nazwa varchar(250) NOT NULL,
21 profil varchar(250) DEFAULT ''
22 );
23 DROP TABLE IF EXISTS uczen;
24 CREATE TABLE IF NOT EXISTS uczen (
25 id INTEGER PRIMARY KEY ASC,
26 imie varchar(250) NOT NULL,
27 nazwisko varchar(250) NOT NULL,
28 grupa_id INTEGER NOT NULL,
29 FOREIGN KEY(grupa_id) REFERENCES grupa(id)
30 )""")
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.
20class Grupa(BazaModel):
21 nazwa = CharField(null=False)
22 profil = CharField(default='')
23
24
25class Uczen(BazaModel):
26 imie = CharField(null=False)
27 nazwisko = CharField(null=False)
28 grupa = ForeignKeyField(Grupa, related_name='uczniowie')
29
30
31baza.connect() # nawiązujemy połączenie z bazą
32baza.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ń:
32# wstawiamy dane uczniów
33cur.execute('INSERT INTO grupa VALUES(NULL, ?, ?);', ('1A', 'matematyczny'))
34cur.execute('INSERT INTO grupa VALUES(NULL, ?, ?);', ('1B', 'humanistyczny'))
35
36# wykonujemy zapytanie SQL, które pobierze id grupy "1A" z tabeli "grupa".
37cur.execute('SELECT id FROM grupa WHERE nazwa = ?', ('1A',))
38grupa_id = cur.fetchone()[0]
39
40# wstawiamy dane uczniów
41cur.execute('INSERT INTO uczen VALUES(?,?,?,?)',
42 (None, 'Tomasz', 'Nowak', grupa_id))
43cur.execute('INSERT INTO uczen VALUES(?,?,?,?)',
44 (None, 'Adam', 'Kowalski', grupa_id))
45
46# zatwierdzamy zmiany w bazie
47con.commit()
W systemie ORM pracujemy z instancjami inst_grupa
i inst_uczen
. Nadajemy wartości ich
atrybutom i korzystamy z ich metod:
34# dodajemy dwie klasy, jeżeli tabela jest pusta
35if Grupa.select().count() == 0:
36 inst_grupa = Grupa(nazwa='1A', profil='matematyczny')
37 inst_grupa.save()
38 inst_grupa = Grupa(nazwa='1B', profil='humanistyczny')
39 inst_grupa.save()
40
41# tworzymy instancję klasy Grupa reprezentującą klasę "1A"
42inst_grupa = Grupa.select().where(Grupa.nazwa == '1A').get()
43# dodajemy uczniów
44inst_uczen = Uczen(imie='Tomasz', nazwisko='Nowak', grupa=inst_grupa)
45inst_uczen.save()
46inst_uczen = Uczen(imie='Adam', nazwisko='Kowalski', grupa=inst_grupa)
47inst_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:
50def czytajdane():
51 """Funkcja pobiera i wyświetla dane z bazy"""
52 cur.execute(
53 """
54 SELECT uczen.id,imie,nazwisko,nazwa FROM uczen,grupa
55 WHERE uczen.grupa_id=grupa.id
56 """)
57 uczniowie = cur.fetchall()
58 for uczen in uczniowie:
59 print(uczen['id'], uczen['imie'], uczen['nazwisko'], uczen['nazwa'])
60 print()
61
62
63czytajdane()
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.
50def czytajdane():
51 """Funkcja pobiera i wyświetla dane z bazy"""
52 for uczen in Uczen.select(): # lub szybsze: Uczen.select().join(Grupa)
53 print(uczen.id, uczen.imie, uczen.nazwisko, uczen.grupa.nazwa)
54 print()
55
56
57czytajdane()
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
.
65# przepisanie ucznia do innej grupy
66cur.execute('SELECT id FROM uczen WHERE nazwisko="Nowak"')
67uczen_id = cur.fetchone()[0]
68cur.execute('SELECT id FROM grupa WHERE nazwa = ?', ('1B',))
69grupa_id = cur.fetchone()[0]
70cur.execute('UPDATE uczen SET grupa_id=? WHERE id=?', (grupa_id, uczen_id))
71czytajdane()
72
73# usunięcie ucznia o identyfikatorze 1
74cur.execute('DELETE FROM uczen WHERE id=?', (1,))
75czytajdane()
76
77con.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:
59# przepisanie ucznia do innej klasy
60inst_uczen = Uczen.select().join(Grupa).where(Uczen.nazwisko == 'Nowak').get()
61inst_uczen.grupa = Grupa.select().where(Grupa.nazwa == '1B').get()
62inst_uczen.save() # zapisanie zmian w bazie
63czytajdane()
64
65# usunięcie ucznia o identyfikatorze 1
66inst_uczen = Uczen.select().where(Uczen.id == 1).get()
67inst_uczen.delete_instance()
68czytajdane()
69
70baza.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
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:
2025-04-12 o 10:21 w Sphinx 7.3.7
- Autorzy: