5.3. System ORM SQLAlchemy
Używanie systemów ORM, takich jak SQLAlchemy, w prostych projektach sprowadza się do schematu, który poglądowo można opisać w trzech krokach:
deklaracja modelu opisującego bazę
utworzenie na podstawie modelu tabel w bazie,
wykonywanie operacji CRUD.
Przez model (zob. też: model bazy danych) rozumiemy tutaj deklaracje klas i ich właściwości (atrybutów) opisujące obiekty, które będą przechowywane w bazie. Systemy ORM na podstawie klas tworzą odpowiednie tabele i pola, uwzględniając ich typy i powiązania. Odwzorowanie klas i ich właściwości na tabele, kolumny i relacje w bazie stanowi istotę mapowania relacyjno-obiektowego.
Poniżej spróbujemy pokazać, jak wykonywać typowe operacje na bazie z wykorzystaniem biblioteki SQLAlchemy.
Informacja
Wyjaśnienia podanego niżej kodu są uproszczone ze względu na przejrzystość i poglądowość instrukcji. Do używania systemów ORM wystarczające jest poznanie ich interfejsu API.
5.3.1. Środowisko pracy
Informacja
Do kodowania i uruchamiania skryptu możesz użyć dowolnych narzędzi, np. ulubionego edytora kodu i terminala. Sugerujemy jednak wykorzystanie środowiska typu PyCharm lub innego, ponieważ ułatwiają przygotowania i pracę nad projektami w języku Python.
Przed rozpoczęciem pracy przygotuj w wybranym katalogu, np. baza_orm` wirtualne środowisko Pythona
i w aktywnym środowisku zainstaluj pakiet SQLAlchemy:
(.venv) ~/baza_orm$ pip install sqlalchemy
5.3.2. Klasa bazowa
W ulubionym edytorze utwórz dwa plik o nazwie orm_sa.py.
1import os
2from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
3from typing import List
4from sqlalchemy import ForeignKey, Integer, String, create_engine
5from sqlalchemy.orm import Session
6
7plik_bazy = 'baza_sa.db'
8if os.path.exists(plik_bazy):
9 os.remove(plik_bazy)
10
11# tworzymy instancję klasy Engine do obsługi bazy
12baza = create_engine('sqlite:///' + plik_bazy) # ':memory:'
13
14# klasa bazowa dla modeli
15class Base(DeclarativeBase):
16 pass
17
Na początku importujemy potrzebne klasy. Dalej tworzymy zmienną plik_bazy,
która będzie przechowywała nazwę pliku z bazą danych.
Jeżeli plik znajduje się na dysku (if os.path.exists()), usuwamy go (os.remove()),
aby zapewnić bezproblemowe działanie skryptu podczas wielokrotnego uruchamiania.
Następnie tworzymy obiekt baza do obsługi bazy SQlite3 przechowywanej w pliku baza_sa.db.
Do utworzenia modeli danych potrzebna będzie klasa bazowa, którą tworzymy w oparciu o klasę
DeclarativeBase.
5.3.3. Model danych
Dodajemy definicje klas opisujących dwa obiekty reprezentujące klasę i ucznia. Każda klasa ma swoją nazwę i profil, każdy uczeń ma imię, nazwisko oraz przynależy do jakiejś klasy.
18# klasy Klasa i Uczen opisują rekordy tabel "klasa" i "uczen" oraz relacje między nimi
19class Klasa(Base):
20 __tablename__ = 'klasa'
21 id: Mapped[int] = mapped_column(Integer, primary_key=True)
22 nazwa: Mapped[str] = mapped_column(String(100), nullable=False)
23 profil: Mapped[str] = mapped_column(String(100), default='')
24 uczniowie: Mapped[List["Uczen"]] = relationship(back_populates='klasa')
25
26
27class Uczen(Base):
28 __tablename__ = 'uczen'
29 id: Mapped[int] = mapped_column(Integer, primary_key=True)
30 imie: Mapped[str] = mapped_column(String(100), nullable=False)
31 nazwisko: Mapped[str] = mapped_column(String(100), nullable=False)
32 klasa_id: Mapped[int] = mapped_column(ForeignKey('klasa.id'), nullable=False)
33 klasa: Mapped["Klasa"] = relationship(back_populates="uczniowie")
34
35# tworzymy tabele
36Base.metadata.create_all(baza)
37
Tworzenie modelu opiera się na dziedziczonej klasie podstawowej Base i mapowaniu deklaratywnym
(ang. Declarative Mapping). Definicje klas o nazwach Klasa i Uczen z jednej strony opisują
obiekty Pythona, z drugiej strony zawierają metainformacje opisujące tabele SQL,
które utworzone zostaną w bazie. Definicje wykorzystują również wskazówki dotyczące typów danych
(ang. type hinst). Przeanalizujmy kilka fragmentów kodu:
__tablename__– określa nazwę tabeli w bazie danych,id: Mapped[int]– nazwa pola z adnotacją typu danych,mapped_column()- funkcja pozwalająca definiować ograniczenia pól tworzonych w tabelach, np.:Integer– pole przechowuje liczby całkowite,String(100)– pole przechowuje maksymalnie 40 znaków,primary_key=True– pole jest kluczem głównym,nullable=False– pole nie może zawierać wartości null,default=''– domyślna wartość pola,ForeignKey()– definiuje klucz obcy, jako argument podajemy nazwę tabeli i klucza głównego,
relationship()– funkcja, która tworzy relację zwrotną między dwoma zmapowanymi klasami podanymi w adnotacji typu, np.:Mapped[List["Uczen"]],Mapped["Klasa"]; argumentback_populatespozwala wskazać nazwę relacji w powiązanej klasie.
Relacja zwrotna pozwala na dostęp do powiązanych obiektów, np. kod typu klasa.uczniowie
da nam dostęp do uczniów należących do danej klasy, a kod uczen.klasa wskaże klasę,
do której należy uczeń.
Zdefiniowane model możemy sprawdzić za pomocą kodu tworzącego tabele: Base.metadata.create_all(baza).
Omówiony kod można uruchomić. W katalogu, z którego uruchamiamy skrypt, powinien zostać utworzony
plik bazy baza_sa.db.
5.3.3.1. Ćwiczenie
Wykorzystaj interpreter sqlite3 i sprawdź, czy zostały utworzone tabele, czyli jak wygląda kod SQL wygenerowany przez ORM. Przykładowy zrzut poniżej.
Informacja
Nazwy utworzonych tabel to nazwy klas, które je opisują, podobnie nazwy pól odpowiadają nazwom atrybutów.
5.3.4. Dodawanie danych
Do pliku orm_sa.py dodajemy następujący kod:
38# tworzymy sesję, która przechowuje obiekty i umożliwia "rozmowę" z bazą
39with Session(baza) as sesja:
40
41 # dodajemy dwie klasy
42 klasa1 = Klasa(nazwa='1A', profil='matematyczny')
43 klasa2 = Klasa(nazwa='1B', profil='humanistyczny')
44 sesja.add(klasa1)
45 sesja.add(klasa2)
46
47 sesja.commit()
48
49 uczniowie = [
50 Uczen(imie='Tomasz', nazwisko='Nowak', klasa_id=klasa1.id),
51 Uczen(imie='Jan', nazwisko='Kos', klasa_id=klasa2.id),
52 Uczen(imie='Piotr', nazwisko='Kowalski', klasa_id=klasa2.id)
53 ]
54 # dodajemy dane wielu uczniów
55 sesja.add_all(uczniowie)
56 sesja.commit()
57
Wykonywanie operacji na bazie danych wymaga utworzenia obiektu sesji:
with Session(baza) as sesja. Użycie konstrukcji with ... as ...
pozwala uniknąć niektórych błędów podczas wykonywania operacji na bazie.
Informacja
Mechanizm sesji jest unikalny dla SQLAlchemy, pozwala wykonywać serię powiązanych
ze sobą operacji na bazie danych w ramach jednej transakcji. Sesja przechowuje
tworzone obiekty i zapamiętuje wykonywane na nich operacje.
W prostych aplikacjach wykorzystuje się jedną instancję sesji,
w bardziej złożonych można korzystać z wielu.
Instancja sesji tworzona jest na podstawie klasy Session z parametrem wskazującym bazę.
Obiekt sesji zawiera metody pozwalające komunikować się z bazą, np. execute(), która
wykonuje zapytania. Jeżeli zmiany w sesji mają zostać zapisane w bazie danych,
trzeba użyć metody commit() do ich zatwierdzenia.
Do tworzenia nowych rekordów używamy metody add(). Jako argument podajemy nazwę modelu
z wymaganymi argumentami.
W ramach sesji można wykonywać wiele operacji, jednak aby zostały odzwierciedlone w bazie danych,
trzeba wywołać metodę commit().
Informacja
Dopiero po zatwierdzeniu zmian metodą commit() mamy dostęp do identyfikatorów nowo
utworzonych obiektów.
Metoda add_all() służy do dodawania wielu rekordów na raz. Jako argument podajemy listę obiektów
uczniowie. Warto zwrócić uwagę, że aby określić klasę, do której należy uczeń, atrybutowi klasa_id
modelu przypisujemy identyfikator obiektu reprezentującego klasę.
5.3.4.1. Ćwiczenie
Ponownie wykonaj dotychczasowy kod i sprawdź za pomocą interpretera
sqlite3, czy w tabelach znalazły się odpowiednie dane.Wskazówka
W interpreterze możesz wykorzystać proste kwerendy SQL, np.:
SELECT * FROM klasa;orazSELECT * FROM uczen;.
5.3.5. Odczyt danych
Odczyt danych może być realizowany na wiele sposobów. Zacznijmy od uzupełnienia kodu skryptu:
58 from sqlalchemy import select, func, delete
59 # odczytujemy wiele rekordów
60 print('Klasy:')
61 zapytanie = select(Klasa)
62 klasy = sesja.execute(zapytanie)
63 for klasa in klasy:
64 print(klasa[0].id, klasa[0].nazwa, klasa[0].profil)
65 print()
66
67 # odczytujemy jeden rekord
68 zapytanie = select(Klasa).where(Klasa.nazwa == '1A')
69 klasa = sesja.scalar(zapytanie)
70 print('Klasa:', klasa.nazwa)
71 print()
72
73 def wypisz_listę_uczniow():
74 """ Odczytujemy i wypisujemy dane uczniów, w tym klasę"""
75 # if sesja.query(Uczen).count():
76 if sesja.execute(select(func.count()).select_from(Uczen)).scalar():
77 print('Uczniowie:')
78 uczniowie = sesja.scalars(select(Uczen).join(Klasa))
79 for uczen in uczniowie:
80 print(uczen.id, uczen.imie, uczen.nazwisko, uczen.klasa.nazwa)
81 print()
82 else:
83 print('Brak uczniów w bazie!')
84
85 wypisz_listę_uczniow()
Do tworzenia zapytań używamy funkcji select(), np.:
select(Klasa)– odczytujemy wszystkie obiekty modeluKlasa,select(Klasa).where(Klasa.nazwa == '1A')– odczytujemy obiekt reprezentujący klasę 1A, metodawhere()odpowiada klauzuliWHEREjęzyka SQL,select(Uczen).join(Klasa)– odczytujemy obiekty modeluUczenrazem z danymi o klasie, do której uczeń należy, metodajoin()odpowiada klauzuliJOINjęzyka SQL.
Zapytania wykonujemy za pomocą metod sesji:
scalars()– zwraca wszystkie pasujące obiekty, które można odczytywać np. w pętlifor,scalar()– zwraca pierwszy element pierwszego zwróconego rekordu lub wyjątekMultipleResultsFound.
W funkcji wypisz_liste_uczniow() do sprawdzenia liczby obiektów zapisanych w bazie używamy zapytania
zawierającego:
funkcję
select(), której argumentem jest funkcjacount()wywoływana z przestrzeni nazwfuncudostępniającej funkcje SQL,metody
select_from(), które pozwala określić źródło danych na podstawie podanego modelu.
Zapytanie wykonujemy za pomocą metody execute(), wynik, tzn. liczbę obiektów,
pobieramy z użyciem metody scalar().
Wskazówka
Omówiony powyżej kod zliczający obiekty, czyli rekordy zapisane w tabeli bazy danych,
charakterystyczny dla SQLAlchemy w wersji 2.x można zastąpić prostszym stosowanym w wersji
1.4, który nadal działa: if sesja.query(Uczen).count():.
5.3.6. Modyfikowanie danych
Systemy ORM ułatwiają modyfikowanie danych w bazie, ponieważ operacja ta polega na zmianie wartości pól wybranego obiektu. W naszym skrypcie dopisujemy kod:
87 # zmiana klasy ucznia o identyfikatorze 2
88 uczen = sesja.scalar(select(Uczen).where(Uczen.id == 2))
89 id_klasa = sesja.scalar(select(Klasa.id).where(Klasa.nazwa == '1A'))
90 print('Zmieniam klasę ucznia:', uczen.imie, uczen.nazwisko, uczen.klasa.nazwa)
91 uczen.klasa_id = id_klasa
92 sesja.commit()
93 wypisz_listę_uczniow()
94
Na początku odczytujemy obiekt klasy Uczen o podanym identyfikatorze.
Następnie wykonujemy zapytanie select(Klasa.id).where(Klasa.nazwa == '1A') za pomocą
metody scalar(), która zwraca identyfikator klasy 1A.
W kolejnym kroku zmieniamy atrybut klasa_id obiektu reprezentującego ucznia.
Ma końcu zatwierdzamy zmiany w bazie danych za pomocą metody commit().
5.3.7. Usuwanie danych
Do skryptu dodajemy kolejna porcję kodu:
95 # usunięcie ucznia o identyfikatorze 3
96 uczen = sesja.get(Uczen, 3)
97 print('Usuwam ucznia:', uczen.id, uczen.imie, uczen.nazwisko)
98 sesja.delete(uczen)
99 sesja.flush()
100 wypisz_listę_uczniow()
101
102 print('Usuwam uczniów z klasy 1A')
103 id_klasa = sesja.scalar(select(Klasa.id).where(Klasa.nazwa == '1A'))
104 zapytanie = delete(Uczen).where(Uczen.klasa_id == id_klasa)
105 sesja.execute(zapytanie)
106 sesja.commit()
107 wypisz_listę_uczniow()
Z użyciem metody get() sesji możemy odczytać obiekt (ucznia), o podanym identyfikatorze (3).
Obiekt usuwamy za pomocą metody delete() sesji. Za pomocą metody flush() przekazujemy
bazie danych zlecenie usunięcia obiektu.
Do usuwania wielu rekordów służy funkcja delete(), któ©a podobnie jak select()
służy do przygotowania zapytania wybierającego rekordy na podstawie kryteriów podanych
jako argumenty metody where(). Zapytanie wykonujemy za pomocą metody execute() sesji.
Na koniec ponownie zatwierdzamy (tj. zapisujemy) zmiany w bazie danych.
5.3.8. Zadania
Spróbuj dodać do bazy korzystając z systemu Peewee wiele rekordów na raz pobranych z pliku
uczniowie.csv. Wykorzystaj i zmodyfikuj funkcjępobierz_dane()opisaną w materiale Dane z pliku.Dodaj do aplikacji konsolowy interfejs, który umożliwi operacje odczytu, zapisu, modyfikowania i usuwania rekordów. Dane powinny być pobierane z klawiatury od użytkownika.
Przedstawione rozwiązania warto użyć w aplikacjach internetowych jako relatywnie szybki i łatwy sposób obsługi danych. Zobacz, jak to zrobić na przykładzie scenariusza aplikacji Quiz ORM.
Przejrzyj scenariusz aplikacji internetowej Czat, zbudowanej z użyciem frameworku Django, korzystającego z własnego modelu ORM.
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: