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:

  1. deklaracja modelu opisującego bazę

  2. utworzenie na podstawie modelu tabel w bazie,

  3. 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.

SQLAlchemy. Kod nr
 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.

SQLAlchemy. Kod nr
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"]; argument back_populates pozwala 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

  1. Wykorzystaj interpreter sqlite3 i sprawdź, czy zostały utworzone tabele, czyli jak wygląda kod SQL wygenerowany przez ORM. Przykładowy zrzut poniżej.

../../_images/sqlite3_21.png

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:

SQLAlchemy. Kod nr
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

  1. 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; oraz SELECT * FROM uczen;.

5.3.5. Odczyt danych

Odczyt danych może być realizowany na wiele sposobów. Zacznijmy od uzupełnienia kodu skryptu:

SQLAlchemy. Kod nr
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 modelu Klasa,

  • select(Klasa).where(Klasa.nazwa == '1A') – odczytujemy obiekt reprezentujący klasę 1A, metoda where() odpowiada klauzuli WHERE języka SQL,

  • select(Uczen).join(Klasa) – odczytujemy obiekty modelu Uczen razem z danymi o klasie, do której uczeń należy, metoda join() odpowiada klauzuli JOIN języka SQL.

Zapytania wykonujemy za pomocą metod sesji:

  • scalars() – zwraca wszystkie pasujące obiekty, które można odczytywać np. w pętli for,

  • scalar() – zwraca pierwszy element pierwszego zwróconego rekordu lub wyjątek MultipleResultsFound.

W funkcji wypisz_liste_uczniow() do sprawdzenia liczby obiektów zapisanych w bazie używamy zapytania zawierającego:

  • funkcję select(), której argumentem jest funkcja count() wywoływana z przestrzeni nazw func udostę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:

SQLAlchemy. Kod nr
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:

SQLAlchemy. Kod nr
 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

  1. 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.

  2. 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.

  3. 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.

  4. Przejrzyj scenariusz aplikacji internetowej Czat, 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