5.2. Systemy ORM¶
Znajomość języka SQL jest oczywiście niezbędna, aby korzystać z wszystkich możliwości baz danych, niemniej w wielu niespecjalistycznych projektach można je obsługiwać inaczej, tj. za pomocą systemów ORM (ang. Object-Relational Mapping – mapowanie obiektowo-relacyjne). Pozwalają one traktować tabele w sposób obiektowy, co bywa wygodniejsze w budowaniu logiki aplikacji.
Używanie systemów ORM, takich jak Peewee czy SQLAlchemy, w prostych projektach sprowadza się do schematu, który poglądowo można opisać w trzech krokach:
- Nawiązanie połączenia z bazą
- Deklaracja modelu opisującego bazę i utworzenie struktury bazy
- Wykonywanie operacji CRUD
Poniżej spróbujemy pokazać, jak operacje wykonywane przy użyciu wbudowanego w Pythona modułu sqlite3 zrealizować przy użyciu technik ORM.
Informacja
Wyjaśnienia podanego niżej kodu są w wielu miejscach uproszczone. Ze względu na przejrzystość i poglądowość instrukcji nie wgłębiamy się w techniczne różnice w implementacji technik ORM w obydwu rozwiązaniach. Poznanie ich interfejsu jest wystarczające, aby efektywnie obsługiwać bazy danych. Co ciekawe, dopóki używamy bazy SQLite3, systemy ORM można traktować jako swego rodzaju nakładkę na owmówiony wyżej moduł sqlite3 wbudowany w Pythona.
5.2.1. Połączenie z bazą¶
W ulubionym edytorze utwórz dwa puste pliki o nazwach ormpw.py
i ormsa.py
.
Pierwszy z nich zawierał będzie kod wykorzystujący ORM Peewee, drugi ORM SQLAlchemy.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | #! /usr/bin/env python3
# -*- 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:'
class BazaModel(Model): # klasa bazowa
class Meta:
database = baza
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | #! /usr/bin/env python3
# -*- coding: utf-8 -*-
import os
from sqlalchemy import Column, ForeignKey, Integer, String, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship, sessionmaker
if os.path.exists('test.db'):
os.remove('test.db')
# tworzymy instancję klasy Engine do obsługi bazy
baza = create_engine('sqlite:///test.db') # ':memory:'
# klasa bazowa
BazaModel = declarative_base()
|
W jednym i drugim przypadku importujemy najpierw potrzebne klasy.
Następnie tworzymy instancje baza
służące do nawiązania połączeń
z bazą przechowywaną w pliku test.db
. Jeżeli zamiast nazwy pliku,
podamy :memory:
bazy umieszczone zostaną w pamięci RAM (przydatne
podczas testowania).
Informacja
Moduły os
i sys
nie są niezbędne do działania prezentowanego kodu,
ale można z nich skorzystać, kiedy chcemy sprawdzić obecność pliku na
dysku (os.path.ispath()
) lub zatrzymać wykonywanie skryptu w dowolnym
miejscu (sys.exit()
). W podanych przykładach usuwamy plik bazy,
jeżeli znajduje się na dysku, aby zapewnić bezproblemowe działanie
kompletnych skryptów.
5.2.2. Model danych i baza¶
Przez model rozumiemy tutaj deklaracje klas i ich właściwości (atrybutów) opisujące obiekty, którymi się zajmujemy. Systemy ORM na podstawie klas tworzą odpowiednie tablice, pola, uwzględniając ich typy i powiązania. Wzajemne powiązanie klas i ich właściwości z tabelami i kolumnami w bazie stanowi właśnie istotę mapowania relacyjno-obiektowego.
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | # klasy Klasa i Uczen opisują rekordy tabel "klasa" i "uczen"
# oraz relacje między nimi
class Klasa(BazaModel):
nazwa = CharField(null=False)
profil = CharField(default='')
class Uczen(BazaModel):
imie = CharField(null=False)
nazwisko = CharField(null=False)
klasa = ForeignKeyField(Klasa, related_name='uczniowie')
baza.connect() # nawiązujemy połączenie z bazą
baza.create_tables([Klasa, Uczen], True) # tworzymy tabele
|
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | # klasy Klasa i Uczen opisują rekordy tabel "klasa" i "uczen"
# oraz relacje między nimi
class Klasa(BazaModel):
__tablename__ = 'klasa'
id = Column(Integer, primary_key=True)
nazwa = Column(String(100), nullable=False)
profil = Column(String(100), default='')
uczniowie = relationship('Uczen', backref='klasa')
class Uczen(BazaModel):
__tablename__ = 'uczen'
id = Column(Integer, primary_key=True)
imie = Column(String(100), nullable=False)
nazwisko = Column(String(100), nullable=False)
klasa_id = Column(Integer, ForeignKey('klasa.id'))
# tworzymy tabele
BazaModel.metadata.create_all(baza)
|
W obydwu przypadkach deklarowanie modelu opiera się na pewnej “klasie” podstawowej,
którą nazwaliśmy BazaModel
. Dziedzicząc z niej, deklarujemy następnie
własne klasy o nazwach Klasa i Uczen reprezentujące tabele w bazie.
Właściwości tych klas odpowiadają kolumnom; w SQLAlchemy używamy nawet
klasy o nazwie Column()
, która wyraźnie wskazuje na rodzaj tworzonego atrybutu.
Obydwa systemy wymagają określenia typu danych definiowanych pól. Służą temu odpowiednie
klasy, np. CharField()
lub String()
. Możemy również definiować dodatkowe
cechy pól, takie jak np. nie zezwalanie na wartości puste (null=False
lub nullable=False
)
lub określenie wartości domyślnych (default=''
).
Warto zwrócić uwagę, na sposób określania relacji. W Peewee używamy
konstruktora klasy: ForeignKeyField(Klasa, related_name = 'uczniowie')
.
Przyjmuje on nazwę klasy powiązanej, z którą tworzymy relację, i nazwę atrybutu
określającego relację zwrotną w powiązanej klasie. Dzięki temu
wywołanie w postaci Klasa.uczniowie
da nam dostęp do obiektów
reprezentujących uczniów przypisanych do danej klasy. Zuważmy, że Peewee
nie wymaga definiowania kluczy głównych, są tworzone automatycznie
pod nazwą id
.
W SQLAlchemy dla odmiany nie tylko jawnie określamy klucze główne
(primary_key=True
), ale i podajemy nazwy tabel (__tablename__ = 'klasa'
).
Klucz obcy oznaczamy odpowiednim parametrem w klasie definiującej pole
(Column(Integer, ForeignKey('klasa.id'))
). Relację zwrotną
tworzymy za pomocą konstruktora relationship('Uczen', backref='klasa')
,
w którym podajemy nazwę powiązanej klasy i nazwę atrybutu tworzącego
powiązanie. W tym wypadku wywołanie typu uczen.klasa
udostępni obiekt
reprezentujący klasę, do której przypisano ucznia.
Po zdefiniowaniu przemyślanego modelu, co jest relatywnie najtrudniejsze,
trzeba przetestować działanie mechanizmów ORM w praktyce, czyli utworzyć
tabele i kolumny w bazie. W Peewee łączymy się z bazą i wywołujemy
metodę .create_tables()
, której podajemy nazwy klas reprezentujących
tabele. Dodatkowy parametr True
powoduje sprawdzenie przed utworzeniem,
czy tablic w bazie już nie ma. SQLAlchemy wymaga tylko wywołania metody
.create_all()
kontenera metadata zawartego w klasie bazowej.
Podane kody można już uruchomić, oba powinny utworzyć bazę test.db
w katalogu, z którego uruchamiamy skrypt.
Informacja
Warto wykorzystać interpreter sqlite3 i sprawdzić, jak wygląda kod tworzący tabele wygenerowany przez ORM-y. Poniżej przykład ilustrujący SQLAlchemy.

5.2.3. Operacje CRUD¶
5.2.3.1. Wstawianie i odczytywanie danych¶
Podstawowe operacje wykonywane na bazie, np, wstawianie i odczytywanie danych, w Peewee wykonywane są za pomocą obiektów reprezentujących rekordy zdefiniowanych tabel oraz ich metod. W SQLAlchemy oprócz obiektów wykorzystujemy metody sesji, w ramach której komunikujemy się z bazą.
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | # dodajemy dwie klasy, jeżeli tabela jest pusta
if Klasa().select().count() == 0:
inst_klasa = Klasa(nazwa='1A', profil='matematyczny')
inst_klasa.save()
inst_klasa = Klasa(nazwa='1B', profil='humanistyczny')
inst_klasa.save()
# tworzymy instancję klasy Klasa reprezentującą klasę "1A"
inst_klasa = Klasa.select().where(Klasa.nazwa == '1A').get()
# lista uczniów, których dane zapisane są w słownikach
uczniowie = [
{'imie': 'Tomasz', 'nazwisko': 'Nowak', 'klasa': inst_klasa},
{'imie': 'Jan', 'nazwisko': 'Kos', 'klasa': inst_klasa},
{'imie': 'Piotr', 'nazwisko': 'Kowalski', 'klasa': inst_klasa}
]
# dodajemy dane wielu uczniów
Uczen.insert_many(uczniowie).execute()
# odczytujemy dane z bazy
def czytajdane():
for uczen in Uczen.select().join(Klasa):
print(uczen.id, uczen.imie, uczen.nazwisko, uczen.klasa.nazwa)
print()
czytajdane()
|
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | # tworzymy sesję, która przechowuje obiekty i umożliwia "rozmowę" z bazą
BDSesja = sessionmaker(bind=baza)
sesja = BDSesja()
# dodajemy dwie klasy, jeżeli tabela jest pusta
if not sesja.query(Klasa).count():
sesja.add(Klasa(nazwa='1A', profil='matematyczny'))
sesja.add(Klasa(nazwa='1B', profil='humanistyczny'))
# tworzymy instancję klasy Klasa reprezentującą klasę "1A"
inst_klasa = sesja.query(Klasa).filter_by(nazwa='1A').one()
# dodajemy dane wielu uczniów
sesja.add_all([
Uczen(imie='Tomasz', nazwisko='Nowak', klasa_id=inst_klasa.id),
Uczen(imie='Jan', nazwisko='Kos', klasa_id=inst_klasa.id),
Uczen(imie='Piotr', nazwisko='Kowalski', klasa_id=inst_klasa.id),
])
def czytajdane():
for uczen in sesja.query(Uczen).join(Klasa).all():
print(uczen.id, uczen.imie, uczen.nazwisko, uczen.klasa.nazwa)
print()
czytajdane()
|
Dodawanie informacji w systemach ORM polega na utworzeniu instancji odpowiedniego
obiektu i podaniu w jego konstruktorze wartości atrybutów reprezentujących pola rekordu:
Klasa(nazwa = '1A', profil = 'matematyczny')
. Utworzony rekord zapisujemy metodą
.save()
obiektu w Peewee lub metodą .add()
sesji w SQLAlchemy.
Można również dodawać wiele rekordów na raz. Peewee oferuje metodę .insert_many()
,
która jako parametr przyjmuje listę słowników zawierających dane w formacie
“klucz”:”wartość”, przy czym kluczem jest nazwa pola klasy (tabeli).
SQLAlchemy ma metodę .add_all()
wymagającą listy konstruktorów obiektów,
które chcemy dodać.
Zanim dodamy pierwsze informacje sprawdzamy, czy w tabeli klasa są jakieś wpisy, a więc
wykonujemy prostą kwerendę zliczającą. Peewee używa
metod odpowiednich obiektów: Klasa().select().count()
, natomiast
SQLAlchemy korzysta metody .query()
sesji, która pozwala pobierać dane
z określonej jako klasa tabeli. Obydwa rozwiązania umożliwiają łańcuchowe
wywoływanie charakterytycznych dla kwerend operacji poprzez “doklejanie”
kolejnych metod, np. sesja.query(Klasa).count()
.
Tak właśnie konstruujemy kwerendy warunkowe. W Peewee definiujemy warunki jako
prametry metody .where(Klasa.nazwa == '1A')
. Podobnie w SQLAlchemy,
tyle, że metody sesji inaczej się nazywają i przyjmują postać
.filter_by(nazwa = '1A')
lub .filter(Klasa.nazwa == '1A')
. Pierwsza
wymaga podania warunku w formacie “klucz”=”wartość”, druga w postaci
wyrażenia SQL (należy uważać na użycie poprawnego operatora ==
).
Pobieranie danych z wielu tabel połączonych relacjami może być w porównaniu
do zapytań SQL-a bardzo proste. W zależności od ORM-a wystarcza polecenie:
Uczen.select()
lub sesja.query(Uczen).all()
, ale przy próbie
odczytu klasy, do której przypisano ucznia (inst_uczen.klasa.nazwa
),
wykonane zostanie dodatkowe zapytanie, co nie jest efektywne.
Dlatego lepiej otwarcie wskazywać na powiązania między obiektami,
czyli w zależności od ORM-u używać:
Uczen.select().join(Klasa)
lub sesja.query(Uczen).join(Klasa).all()
.
Tak właśnie postępujemy w bliźniaczych funkcjach czytajdane()
, które
pokazują, jak pobierać i wyświetlać wszystkie rekordy z tabel powiązanych
relacjami.
Systemy ORM oferują pewne ułatwiania w zależności od tego, ile rekordów lub pól i w jakiej formie chcemy wydobyć. Metody w Peewee:
.get()
- zwraca pojedynczy rekord pasujący do zapytania lub wyjątekDoesNotExist
, jeżeli go brak;.first()
- zwróci z kolei pierwszy rekord ze wszystkich pasujących.
Metody SQLAlchemy:
.get(id)
- zwraca pojedynczy rekord na podstawie podanego identyfikatora;.one()
- zwraca pojedynczy rekord pasujący do zapytania lub wyjątekDoesNotExist
, jeżeli go brak;.scalar()
- zwraca pierwszy element pierwszego zwróconego rekordu lub wyjątekMultipleResultsFound
;.all()
- zwraca pasujące rekordy w postaci listy.
Informacja
Mechanizm sesji jest unikalny dla SQLAlchemy, pozwala m. in. zarządzać
transakcjami i połączeniami z wieloma bazami. Stanowi “przechowalnię”
dla tworzonych obiektów, zapamiętuje wykonywane na nich operacje,
które mogą zostać zapisane w bazie lub w razie potrzeby odrzucone.
W prostych aplikacjach wykorzystuje się jedną instancję sesji,
w bardziej złożonych można korzystać z wielu.
Instancja sesji (sesja = BDSesja()
) tworzona jest na podstawie klasy, która z kolei
powstaje przez wywołanie konstruktora z opcjonalnym parametrem
wskazującym bazę: BDSesja = sessionmaker(bind=baza)
. Jak pokazano
wyżej, obiekt sesji zawiera metody pozwalające komunikować się
z bazą. Warto również zauważyć, że po wykonaniu wszystkich zamierzonych
operacji w ramach sesji zapisujemy dane do bazy wywołując polecenie
sesja.commit()
.
5.2.3.2. Modyfikowanie i usuwanie danych¶
Systemy ORM ułatwiają modyfikowanie i usuwanie danych z bazy, ponieważ operacje te sprowadzają się do zmiany wartości pól klasy reprezentującej tabelę lub do usunięcia instancji danej klasy.
65 66 67 68 69 70 71 72 73 74 75 | # zmiana klasy ucznia o identyfikatorze 2
inst_uczen = Uczen().select().join(Klasa).where(Uczen.id == 2).get()
inst_uczen.klasa = Klasa.select().where(Klasa.nazwa == '1B').get()
inst_uczen.save() # zapisanie zmian w bazie
# usunięcie ucznia o identyfikatorze 3
Uczen.select().where(Uczen.id == 3).get().delete_instance()
czytajdane()
baza.close()
|
67 68 69 70 71 72 73 74 75 76 77 78 79 | # zmiana klasy ucznia o identyfikatorze 2
inst_uczen = sesja.query(Uczen).filter(Uczen.id == 2).one()
inst_uczen.klasa_id = sesja.query(Klasa.id).filter(
Klasa.nazwa == '1B').scalar()
# usunięcie ucznia o identyfikatorze 3
sesja.delete(sesja.query(Uczen).get(3))
czytajdane()
# zapisanie zmian w bazie i zamknięcie sesji
sesja.commit()
sesja.close()
|
Załóżmy, że chcemy zmienić przypisanie ucznia do klasy. W obydwu systemach tworzymy więc obiekt reprezentujący ucznia o identyfikatorze “2”. Stosujemy omówione wyżej metody zapytań. W następnym kroku modyfikujemy odpowiednie pole tworzące relację z tabelą “klasy”, do którego przypisujemy pobrany w zapytaniu obiekt (Peewee) lub identyfikator (SQLAlchemy). Różnice, tzn. przypisywanie obiektu lub identyfikatora, wynikają ze sposobu definiowania modeli w obu rozwiązanich.
Usuwanie jest jeszcze prostsze. W Peewee wystarczy do zapytania zwracającego
obiekt reprezentujący ucznia o podanym id “dokleić” odpowiednią metodę:
Uczen.select().where(Uczen.id == 3).get().delete_instance()
.
W SQLAlchemy korzystamy jak zwykle z metody sesji, której przekazujemy
obiekt reprezentujący ucznia: sesja.delete(sesja.query(Uczen).get(3))
.
Po zakończeniu operacji wykonywanych na danych powinniśmy pamiętać o zamknięciu
połączenia, robimy to używając metody obiektu bazy baza.close()
(Peewee)
lub sesji sesja.close()
(SQLAlchemy). UWAGA: operacje dokonywane
podczas sesji w SQLAlchemy muszą zostać zapisane w bazie, dlatego przed
zamknięciem połączenia trzeba umieścić polecenie sesja.commit()
.
5.2.4. Zadania¶
- Spróbuj dodać do bazy korzystając z systemu Peewee lub SQLAlchemy
wiele rekordów na raz pobranych z pliku. Wykorzystaj i zmodyfikuj
funkcję
pobierz_dane()
opisaną w materiale Dane z pliku. - Postaraj się przedstawione aplikacje wyposażyć w 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 wykorzystaniem frameworku Django, korzystającego z własnego modelu ORM.
5.2.5. Ź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: | 2022-05-22 o 19:52 w Sphinx 1.5.3 |
---|---|
Autorzy: | Patrz plik “Autorzy” |