W części 1 tej serii użyłeś Flask i Connexion do stworzenia interfejsu API REST zapewniającego operacje CRUD w prostej strukturze w pamięci o nazwie PEOPLE
. To zadziałało, aby zademonstrować, w jaki sposób moduł Connexion pomaga w tworzeniu ładnego interfejsu API REST wraz z interaktywną dokumentacją.
Jak niektórzy zauważyli w komentarzach do Części 1, PEOPLE
struktura jest ponownie inicjowana przy każdym ponownym uruchomieniu aplikacji. W tym artykule dowiesz się, jak przechowywać PEOPLE
struktury i działań zapewnianych przez API do bazy danych przy użyciu SQLAlchemy i Marshmallow.
SQLAlchemy udostępnia obiektowy model relacyjny (ORM), który przechowuje obiekty Pythona w reprezentacji bazy danych zawierającej dane obiektu. To może pomóc ci w dalszym myśleniu w pythoniczny sposób i nie martwić się o to, jak dane obiektu będą reprezentowane w bazie danych.
Marshmallow zapewnia funkcjonalność serializacji i deserializacji obiektów Pythona, gdy wychodzą one z i do naszego interfejsu API REST opartego na JSON. Marshmallow konwertuje instancje klas Pythona na obiekty, które można przekonwertować na JSON.
Kod Pythona do tego artykułu znajdziesz tutaj.
Bezpłatny bonus: Kliknij tutaj, aby pobrać kopię przewodnika „Przykłady API REST” i uzyskać praktyczne wprowadzenie do zasad Pythona + API REST wraz z praktycznymi przykładami.
Dla kogo jest ten artykuł
Jeśli podobała Ci się część 1 tej serii, ten artykuł jeszcze bardziej rozszerzy Twój pasek narzędziowy. Będziesz używać SQLAlchemy, aby uzyskać dostęp do bazy danych w sposób bardziej Pythonowy niż prosty SQL. Będziesz także używać Marshmallow do serializacji i deserializacji danych zarządzanych przez REST API. Aby to zrobić, będziesz korzystać z podstawowych funkcji programowania obiektowego dostępnych w Pythonie.
Będziesz także używać SQLAlchemy do tworzenia bazy danych oraz do interakcji z nią. Jest to konieczne, aby uruchomić REST API z PEOPLE
dane użyte w Części 1.
Aplikacja internetowa przedstawiona w części 1 będzie miała swoje pliki HTML i JavaScript zmodyfikowane w drobny sposób, aby wspierać również zmiany. Tutaj możesz przejrzeć ostateczną wersję kodu z Części 1.
Dodatkowe zależności
Zanim zaczniesz budować tę nową funkcjonalność, musisz zaktualizować virtualenv, który utworzyłeś, aby uruchomić kod części 1, lub utworzyć nowy dla tego projektu. Najprostszym sposobem na zrobienie tego po aktywacji virtualenv jest uruchomienie tego polecenia:
$ pip install Flask-SQLAlchemy flask-marshmallow marshmallow-sqlalchemy marshmallow
To dodaje więcej funkcji do wirtualnego środowiska:
-
Flask-SQLAlchemy
dodaje SQLAlchemy, wraz z niektórymi powiązaniami z Flask, umożliwiając programom dostęp do baz danych. -
flask-marshmallow
dodaje części Flask Marshmallow, które pozwalają programom konwertować obiekty Pythona do i ze struktur możliwych do serializacji. -
marshmallow-sqlalchemy
dodaje kilka zaczepów Marshmallow do SQLAlchemy, aby umożliwić programom serializację i deserializację obiektów Pythona generowanych przez SQLAlchemy. -
marshmallow
dodaje większość funkcji Marshmallow.
Dane osób
Jak wspomniano powyżej, PEOPLE
Struktura danych w poprzednim artykule to słownik języka Python w pamięci. W tym słowniku użyłeś nazwiska osoby jako klucza wyszukiwania. Struktura danych w kodzie wyglądała tak:
# Data to serve with our API
PEOPLE = {
"Farrell": {
"fname": "Doug",
"lname": "Farrell",
"timestamp": get_timestamp()
},
"Brockman": {
"fname": "Kent",
"lname": "Brockman",
"timestamp": get_timestamp()
},
"Easter": {
"fname": "Bunny",
"lname": "Easter",
"timestamp": get_timestamp()
}
}
Modyfikacje, które wprowadzisz w programie, przeniosą wszystkie dane do tabeli bazy danych. Oznacza to, że dane zostaną zapisane na dysku i będą istnieć między uruchomieniami server.py
program.
Ponieważ nazwisko było kluczem w słowniku, kod ograniczał możliwość zmiany nazwiska osoby:można było zmienić tylko imię. Ponadto przejście do bazy danych umożliwi zmianę nazwiska, ponieważ nie będzie ono już używane jako klucz wyszukiwania osoby.
Koncepcyjnie tabelę bazy danych można traktować jako dwuwymiarową tablicę, w której wiersze są rekordami, a kolumny są polami w tych rekordach.
Tabele bazy danych zwykle mają automatycznie zwiększającą się wartość całkowitą jako klucz wyszukiwania wierszy. Nazywa się to kluczem podstawowym. Każdy rekord w tabeli będzie miał klucz podstawowy, którego wartość jest unikalna w całej tabeli. Posiadanie klucza podstawowego niezależnego od danych przechowywanych w tabeli pozwala modyfikować dowolne inne pola w rzędzie.
Uwaga:
Autoinkrementacja klucza podstawowego oznacza, że baza danych dba o:
- Zwiększanie największego istniejącego pola klucza podstawowego za każdym razem, gdy nowy rekord jest wstawiany do tabeli
- Używanie tej wartości jako klucza podstawowego dla nowo wstawionych danych
Gwarantuje to unikalny klucz podstawowy w miarę wzrostu tabeli.
Będziesz postępować zgodnie z konwencją bazy danych nazywania tabeli jako liczby pojedynczej, więc tabela będzie nazywać się person
. Tłumaczenie naszych PEOPLE
struktura powyżej do tabeli bazy danych o nazwie person
daje ci to:
person_id | Nazwa | fname | sygnatura czasowa |
---|---|---|---|
1 | Farrel | Doug | 2018-08-08 21:16:01.888444 |
2 | Brockman | Kent | 2018-08-08 21:16:01.889060 |
3 | Wielkanoc | Królik | 2018-08-08 21:16:01.886834 |
Każda kolumna w tabeli ma nazwę pola w następujący sposób:
person_id
: pole klucza podstawowego dla każdej osobylname
: nazwisko osobyfname
: imię osobytimestamp
: znacznik czasu związany z czynnościami wstawiania/aktualizacji
Interakcja z bazą danych
Użyjesz SQLite jako silnika bazy danych do przechowywania PEOPLE
dane. SQLite jest najbardziej rozpowszechnioną bazą danych na świecie i jest dostarczana z Pythonem za darmo. Jest szybki, wykonuje całą swoją pracę przy użyciu plików i nadaje się do bardzo wielu projektów. Jest to kompletny system zarządzania bazą danych RDBMS (Relational Database Management System), który obejmuje SQL, język wielu systemów baz danych.
Na chwilę wyobraź sobie person
tabela już istnieje w bazie danych SQLite. Jeśli miałeś jakieś doświadczenie z RDBMS, prawdopodobnie znasz SQL, strukturalny język zapytań, którego większość systemów RDBMS używa do interakcji z bazą danych.
W przeciwieństwie do języków programowania, takich jak Python, SQL nie definiuje jak aby uzyskać dane:opisuje co dane są pożądane, pozostawiając jak do silnika bazy danych.
Zapytanie SQL pobierające wszystkie dane w naszej person
tabela posortowana według nazwiska wyglądałaby tak:
SELECT * FROM person ORDER BY 'lname';
Zapytanie to mówi silnikowi bazy danych, aby pobrał wszystkie pola z tabeli osoby i posortował je w domyślnej kolejności rosnącej przy użyciu lname
pole.
Gdybyś miał uruchomić to zapytanie w bazie danych SQLite zawierającej person
tabeli, wynikiem byłby zestaw rekordów zawierający wszystkie wiersze w tabeli, przy czym każdy wiersz zawierałby dane ze wszystkich pól tworzących wiersz. Poniżej znajduje się przykład użycia narzędzia wiersza poleceń SQLite, które uruchamia powyższe zapytanie względem person
tabela bazy danych:
sqlite> SELECT * FROM person ORDER BY lname;
2|Brockman|Kent|2018-08-08 21:16:01.888444
3|Easter|Bunny|2018-08-08 21:16:01.889060
1|Farrell|Doug|2018-08-08 21:16:01.886834
Powyższe dane wyjściowe to lista wszystkich wierszy w person
tabela bazy danych ze znakami kreskowymi („|”) oddzielającymi pola w wierszu, co jest wykonywane przez SQLite na potrzeby wyświetlania.
Python jest w pełni zdolny do łączenia się z wieloma silnikami baz danych i wykonywania powyższego zapytania SQL. Wynikiem najprawdopodobniej byłaby lista krotek. Zewnętrzna lista zawiera wszystkie rekordy w person
stół. Każda pojedyncza wewnętrzna krotka zawierałaby wszystkie dane reprezentujące każde pole zdefiniowane dla wiersza tabeli.
Pobieranie danych w ten sposób nie jest zbyt Pythonowe. Lista rekordów jest w porządku, ale każdy pojedynczy rekord to tylko krotka danych. Od programu zależy znajomość indeksu każdego pola w celu pobrania konkretnego pola. Poniższy kod Pythona używa SQLite, aby zademonstrować, jak uruchomić powyższe zapytanie i wyświetlić dane:
1import sqlite3
2
3conn = sqlite3.connect('people.db')
4cur = conn.cursor()
5cur.execute('SELECT * FROM person ORDER BY lname')
6people = cur.fetchall()
7for person in people:
8 print(f'{person[2]} {person[1]}')
Powyższy program wykonuje następujące czynności:
-
Wiersz 1 importuje
sqlite3
moduł. -
Linia 3 tworzy połączenie z plikiem bazy danych.
-
Wiersz 4 tworzy kursor z połączenia.
-
Wiersz 5 używa kursora do wykonania
SQL
zapytanie wyrażone jako ciąg. -
Linia 6 pobiera wszystkie rekordy zwrócone przez
SQL
zapytanie i przypisuje je dopeople
zmienna. -
Linia 7 i 8 iteruj po
people
wypisz zmienną i wydrukuj imię i nazwisko każdej osoby.
people
zmienna z Wierszu 6 powyżej wyglądałoby tak w Pythonie:
people = [
(2, 'Brockman', 'Kent', '2018-08-08 21:16:01.888444'),
(3, 'Easter', 'Bunny', '2018-08-08 21:16:01.889060'),
(1, 'Farrell', 'Doug', '2018-08-08 21:16:01.886834')
]
Wynik działania powyższego programu wygląda tak:
Kent Brockman
Bunny Easter
Doug Farrell
W powyższym programie musisz wiedzieć, że imię osoby znajduje się w indeksie 2
, a nazwisko osoby znajduje się w indeksie 1
. Co gorsza, wewnętrzna struktura person
musi być również znany za każdym razem, gdy przekazujesz zmienną iteracyjną person
jako parametr funkcji lub metody.
Byłoby znacznie lepiej, gdyby to, co otrzymałeś dla person
był obiektem Pythona, gdzie każde z pól jest atrybutem obiektu. To jedna z rzeczy, które robi SQLAlchemy.
Małe stoliki Bobby
W powyższym programie instrukcja SQL jest prostym ciągiem znaków przekazywanym bezpośrednio do bazy danych w celu wykonania. W tym przypadku nie stanowi to problemu, ponieważ SQL jest literałem łańcuchowym całkowicie pod kontrolą programu. Jednak przypadek użycia interfejsu API REST będzie pobierał dane wejściowe użytkownika z aplikacji sieci Web i używał ich do tworzenia zapytań SQL. Może to otworzyć twoją aplikację do ataku.
Przypomnij sobie z części 1, że REST API pozwala uzyskać jedną person
od PEOPLE
dane wyglądały tak:
GET /api/people/{lname}
Oznacza to, że Twój interfejs API oczekuje zmiennej lname
, w ścieżce punktu końcowego adresu URL, której używa do znalezienia pojedynczej person
. Zmodyfikowanie kodu Pythona SQLite z góry w tym celu wyglądałoby mniej więcej tak:
1lname = 'Farrell'
2cur.execute('SELECT * FROM person WHERE lname = \'{}\''.format(lname))
Powyższy fragment kodu wykonuje następujące czynności:
-
Wiersz 1 ustawia
lname
zmienna na'Farrell'
. Pochodziłoby to ze ścieżki punktu końcowego adresu URL interfejsu API REST. -
Wiersz 2 używa formatowania napisów w Pythonie do tworzenia napisów SQL i ich wykonywania.
Aby wszystko było proste, powyższy kod ustawia lname
zmienna na stałą, ale tak naprawdę pochodziłaby ze ścieżki punktu końcowego adresu URL interfejsu API i może być dowolną wartością podaną przez użytkownika. Kod SQL wygenerowany przez formatowanie ciągu wygląda tak:
SELECT * FROM person WHERE lname = 'Farrell'
Kiedy ten kod SQL jest wykonywany przez bazę danych, przeszukuje person
tabela dla rekordu, w którym nazwisko jest równe 'Farrell'
. To jest zamierzone, ale każdy program, który akceptuje dane wprowadzane przez użytkownika, jest również otwarty dla złośliwych użytkowników. W powyższym programie, gdzie lname
zmienna jest ustawiana na podstawie danych wprowadzonych przez użytkownika, co otwiera program na tzw. atak wstrzyknięcia SQL. To jest pieszczotliwie nazywane Little Bobby Tables:
Na przykład wyobraź sobie złośliwego użytkownika o nazwie REST API w ten sposób:
GET /api/people/Farrell');DROP TABLE person;
Powyższe żądanie REST API ustawia lname
zmienna na 'Farrell');DROP TABLE person;'
, który w powyższym kodzie wygeneruje tę instrukcję SQL:
SELECT * FROM person WHERE lname = 'Farrell');DROP TABLE person;
Powyższa instrukcja SQL jest poprawna i po wykonaniu przez bazę danych znajdzie jeden rekord, w którym lname
pasuje do 'Farrell'
. Następnie znajdzie znak ogranicznika instrukcji SQL ;
i pójdzie prosto i porzuci cały stół. To zasadniczo zniszczyłoby twoją aplikację.
Możesz chronić swój program, oczyszczając wszystkie dane, które otrzymujesz od użytkowników aplikacji. Oczyszczanie danych w tym kontekście oznacza, że program sprawdza dane dostarczone przez użytkownika i upewnia się, że nie zawierają one niczego niebezpiecznego dla programu. Może to być trudne do wykonania we właściwy sposób i musiałoby być zrobione wszędzie tam, gdzie dane użytkownika wchodzą w interakcję z bazą danych.
Jest inny sposób, który jest znacznie prostszy:użyj SQLAlchemy. Oczyści dane użytkownika przed utworzeniem instrukcji SQL. To kolejna duża zaleta i powód, aby używać SQLAlchemy podczas pracy z bazami danych.
Modelowanie danych za pomocą SQLAlchemy
SQLAlchemy to duży projekt i zapewnia wiele funkcji do pracy z bazami danych przy użyciu Pythona. Jedną z rzeczy, które zapewnia, jest ORM, czyli Object Relational Mapper, i to jest to, czego będziesz używać do tworzenia i pracy z person
tabela bazy danych. Pozwala to zmapować wiersz pól z tabeli bazy danych na obiekt Pythona.
Programowanie zorientowane obiektowo umożliwia łączenie danych wraz z zachowaniem, funkcjami, które operują na tych danych. Tworząc klasy SQLAlchemy, możesz połączyć pola z wierszy tabeli bazy danych z zachowaniem, umożliwiając interakcję z danymi. Oto definicja klasy SQLAlchemy dla danych w person
tabela bazy danych:
class Person(db.Model):
__tablename__ = 'person'
person_id = db.Column(db.Integer,
primary_key=True)
lname = db.Column(db.String)
fname = db.Column(db.String)
timestamp = db.Column(db.DateTime,
default=datetime.utcnow,
onupdate=datetime.utcnow)
Klasa Person
dziedziczy z db.Model
, do którego dojdziesz, gdy zaczniesz budować kod programu. Na razie oznacza to, że dziedziczysz po klasie bazowej o nazwie Model
, zapewniając atrybuty i funkcje wspólne dla wszystkich klas z niego wywodzących się.
Pozostałe definicje to atrybuty na poziomie klasy zdefiniowane w następujący sposób:
-
__tablename__ = 'person'
łączy definicję klasy zperson
tabela bazy danych. -
person_id = db.Column(db.Integer, primary_key=True)
tworzy kolumnę bazy danych zawierającą liczbę całkowitą pełniącą rolę klucza podstawowego dla tabeli. To również informuje bazę danych, żeperson_id
będzie automatycznie zwiększającą się wartością typu Integer. -
lname = db.Column(db.String)
tworzy pole nazwiska, kolumnę bazy danych zawierającą wartość ciągu. -
fname = db.Column(db.String)
tworzy pole imienia, kolumnę bazy danych zawierającą wartość ciągu. -
timestamp = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
tworzy pole znacznika czasu, kolumnę bazy danych zawierającą wartość daty/czasu.default=datetime.utcnow
parametr domyślnie ustawia wartość znacznika czasu na bieżącąutcnow
wartość podczas tworzenia rekordu.onupdate=datetime.utcnow
parametr aktualizuje znacznik czasu z bieżącymutcnow
wartość, gdy rekord jest aktualizowany.
Uwaga:znaczniki czasu UTC
Być może zastanawiasz się, dlaczego znacznik czasu w powyższej klasie ma wartość domyślną i jest aktualizowany przez datetime.utcnow()
metoda, która zwraca czas UTC lub uniwersalny czas koordynowany. Jest to sposób na standaryzację źródła sygnatury czasowej.
Źródłem lub czasem zerowym jest linia biegnąca na północ i południe od północnego do południowego bieguna Ziemi przez Wielką Brytanię. Jest to zerowa strefa czasowa, od której wszystkie inne strefy czasowe są przesunięte. Używając tego jako źródła czasu zerowego, Twoje znaczniki czasu są przesunięte względem tego standardowego punktu odniesienia.
Jeśli Twoja aplikacja jest dostępna z różnych stref czasowych, masz możliwość wykonania obliczeń daty/czasu. Wszystko czego potrzebujesz to znacznik czasu UTC i docelowa strefa czasowa.
Gdybyś miał użyć lokalnych stref czasowych jako źródła sygnatury czasowej, nie mógłbyś wykonać obliczeń daty/godziny bez informacji o lokalnych strefach czasowych przesuniętych od czasu zerowego. Bez informacji o źródle sygnatury czasowej nie można w ogóle porównywać daty/godziny ani wykonywać obliczeń.
Praca ze znacznikami czasu opartymi na UTC to dobry standard do naśladowania. Oto witryna z zestawem narzędzi do pracy i lepszego ich zrozumienia.
Dokąd zmierzasz z tą person
definicja klasy? Ostatecznym celem jest możliwość uruchomienia zapytania przy użyciu SQLAlchemy i odzyskanie listy wystąpień Person
klasa. Jako przykład spójrzmy na poprzednią instrukcję SQL:
SELECT * FROM people ORDER BY lname;
Pokaż ten sam mały przykładowy program z powyższego, ale teraz używający SQLAlchemy:
1from models import Person
2
3people = Person.query.order_by(Person.lname).all()
4for person in people:
5 print(f'{person.fname} {person.lname}')
Ignorując na razie linię 1, chcesz tylko person
rekordy posortowane w porządku rosnącym według lname
pole. Co otrzymasz z instrukcji SQLAlchemy Person.query.order_by(Person.lname).all()
to lista person
obiekty dla wszystkich rekordów w person
tabeli bazy danych w tej kolejności. W powyższym programie people
zmienna zawiera listę person
obiekty.
Program iteruje po people
zmienna, biorąc każdą person
kolejno i wydrukowanie imienia i nazwiska osoby z bazy danych. Zauważ, że program nie musi używać indeksów, aby uzyskać fname
lub lname
wartości:używa atrybutów zdefiniowanych w Person
obiekt.
Używanie SQLAlchemy pozwala myśleć w kategoriach obiektów z zachowaniem, a nie surowego SQL
. Staje się to jeszcze bardziej korzystne, gdy tabele bazy danych stają się większe, a interakcje bardziej złożone.
Serializowanie/deserializowanie modelowanych danych
Praca z danymi modelowanymi za pomocą SQLAlchemy w programach jest bardzo wygodna. Jest to szczególnie wygodne w programach manipulujących danymi, na przykład wykonujących obliczenia lub wykorzystujących je do tworzenia prezentacji na ekranie. Twoja aplikacja jest interfejsem API REST zasadniczo zapewniającym operacje CRUD na danych i jako taka nie wykonuje wielu manipulacji danymi.
Interfejs API REST działa z danymi JSON i tutaj możesz napotkać problem z modelem SQLAlchemy. Ponieważ dane zwracane przez SQLAlchemy są instancjami klas Pythona, Connexion nie może serializować tych instancji klas do danych w formacie JSON. Pamiętaj z części 1, że Connexion jest narzędziem, którego używałeś do projektowania i konfigurowania interfejsu API REST przy użyciu pliku YAML oraz łączenia z nim metod Pythona.
W tym kontekście serializacja oznacza konwersję obiektów Pythona, które mogą zawierać inne obiekty Pythona i złożone typy danych, na prostsze struktury danych, które można przetworzyć na typy danych JSON, które wymieniono tutaj:
string
: typ ciągunumber
: liczby obsługiwane przez Pythona (liczby całkowite, zmiennoprzecinkowe, długie)object
: obiekt JSON, który jest mniej więcej odpowiednikiem słownika Pythonaarray
: mniej więcej odpowiednik listy Pythonaboolean
: reprezentowane w JSON jakotrue
lubfalse
, ale w Pythonie jakoTrue
lubFalse
null
: zasadniczoNone
w Pythonie
Jako przykład, Twoja Person
klasa zawiera znacznik czasu, którym jest Python DateTime
. W JSON nie ma definicji daty/godziny, więc znacznik czasu musi zostać przekonwertowany na ciąg, aby istniał w strukturze JSON.
Twoja Person
Klasa jest wystarczająco prosta, więc pobranie z niej atrybutów danych i ręczne utworzenie słownika do powrotu z naszych punktów końcowych adresu URL REST nie byłoby trudne. W bardziej złożonej aplikacji z wieloma większymi modelami SQLAlchemy tak nie byłoby. Lepszym rozwiązaniem jest użycie modułu o nazwie Marshmallow, który wykona tę pracę za Ciebie.
Marshmallow pomaga stworzyć PersonSchema
klasa, która jest jak SQLAlchemy Person
klasa, którą stworzyliśmy. Tutaj jednak, zamiast mapować tabele bazy danych i nazwy pól na klasę i jej atrybuty, PersonSchema
class definiuje, w jaki sposób atrybuty klasy zostaną przekonwertowane na formaty przyjazne dla JSON. Oto definicja klasy Marshmallow dla danych w naszej person
tabela:
class PersonSchema(ma.ModelSchema):
class Meta:
model = Person
sqla_session = db.session
Klasa PersonSchema
dziedziczy z ma.ModelSchema
, do którego dojdziesz, gdy zaczniesz budować kod programu. Na razie oznacza to PersonSchema
dziedziczy z klasy bazowej Marshmallow o nazwie ModelSchema
, zapewniając atrybuty i funkcje wspólne dla wszystkich klas z niego wywodzących się.
Reszta definicji jest następująca:
-
class Meta
definiuje klasę o nazwieMeta
w twojej klasie.ModelSchema
klasy, którąPersonSchema
klasa dziedziczy po wyglądach tej wewnętrznejMeta
i używa jej do znalezienia modelu SQLAlchemyPerson
idb.session
. W ten sposób Marshmallow znajduje atrybuty wPerson
klasę i typ tych atrybutów, aby wiedział, jak je serializować/deserializować. -
model
mówi klasie, jakiego modelu SQLAlchemy użyć do serializacji/deserializacji danych do iz. -
db.session
mówi klasie, jakiej sesji bazy danych użyć do introspekcji i określenia typów danych atrybutów.
Dokąd zmierzasz z tą definicją klasy? Chcesz mieć możliwość serializacji wystąpienia person
klasy do danych JSON oraz do deserializacji danych JSON i utworzenia Person
instancje klas z niego.
Utwórz zainicjowaną bazę danych
SQLAlchemy obsługuje wiele interakcji specyficznych dla poszczególnych baz danych i pozwala skoncentrować się na modelach danych oraz na tym, jak z nich korzystać.
Teraz, gdy zamierzasz utworzyć bazę danych, jak wspomniano wcześniej, użyjesz SQLite. Robisz to z kilku powodów. Jest dostarczany z Pythonem i nie musi być instalowany jako oddzielny moduł. Zapisuje wszystkie informacje z bazy danych w jednym pliku, dzięki czemu jest łatwy w konfiguracji i obsłudze.
Zainstalowanie oddzielnego serwera bazy danych, takiego jak MySQL lub PostgreSQL, działałoby dobrze, ale wymagałoby zainstalowania tych systemów i uruchomienia ich, co wykracza poza zakres tego artykułu.
Ponieważ SQLAlchemy obsługuje bazę danych, pod wieloma względami tak naprawdę nie ma znaczenia, jaka jest baza danych.
Zamierzasz utworzyć nowy program narzędziowy o nazwie build_database.py
aby utworzyć i zainicjować SQLite people.db
plik bazy danych zawierający Twoją person
tabela bazy danych. Po drodze utworzysz dwa moduły Pythona, config.py
i models.py
, który będzie używany przez build_database.py
i zmodyfikowany server.py
z części 1.
Oto, gdzie możesz znaleźć kod źródłowy modułów, które zamierzasz utworzyć, które są przedstawione tutaj:
-
config.py
pobiera niezbędne moduły zaimportowane do programu i skonfigurowane. Obejmuje to Flask, Connexion, SQLAlchemy i Marshmallow. Ponieważ będzie używany zarówno przezbuild_database.py
iserver.py
, niektóre części konfiguracji będą dotyczyć tylkoserver.py
aplikacji. -
models.py
to moduł, w którym utworzyszperson
SQLAlchemy iPersonSchema
Definicje klas Marshmallow opisane powyżej. Ten moduł jest zależny odconfig.py
dla niektórych obiektów tam utworzonych i skonfigurowanych.
Moduł konfiguracyjny
config.py
moduł, jak sama nazwa wskazuje, jest miejscem, w którym tworzone i inicjowane są wszystkie informacje konfiguracyjne. Będziemy używać tego modułu zarówno dla naszego build_database.py
plik programu i wkrótce zostanie zaktualizowany server.py
plik z części 1. artykułu. Oznacza to, że skonfigurujemy tutaj Flask, Connexion, SQLAlchemy i Marshmallow.
Mimo że build_database.py
program nie korzysta z Flask, Connexion ani Marshmallow, używa SQLAlchemy do tworzenia naszego połączenia z bazą danych SQLite. Oto kod dla config.py
moduł:
1import os
2import connexion
3from flask_sqlalchemy import SQLAlchemy
4from flask_marshmallow import Marshmallow
5
6basedir = os.path.abspath(os.path.dirname(__file__))
7
8# Create the Connexion application instance
9connex_app = connexion.App(__name__, specification_dir=basedir)
10
11# Get the underlying Flask app instance
12app = connex_app.app
13
14# Configure the SQLAlchemy part of the app instance
15app.config['SQLALCHEMY_ECHO'] = True
16app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////' + os.path.join(basedir, 'people.db')
17app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
18
19# Create the SQLAlchemy db instance
20db = SQLAlchemy(app)
21
22# Initialize Marshmallow
23ma = Marshmallow(app)
Oto, co robi powyższy kod:
-
Linie 2–4 zaimportuj Connexion tak jak w pliku
server.py
program z części 1. Importuje równieżSQLAlchemy
zflask_sqlalchemy
moduł. Daje to dostęp do bazy danych programu. Na koniec importujeMarshmallow
zflask_marshamllow
moduł. -
Linia 6 tworzy zmienną
basedir
wskazując na katalog, w którym działa program. -
Linia 9 używa
basedir
zmienna, aby utworzyć instancję aplikacji Connexion i nadać jej ścieżkę doswagger.yml
plik. -
Linia 12 tworzy zmienną
app
, czyli instancja Flask zainicjowana przez Connexion. -
Linie 15 używa
app
zmienna do konfiguracji wartości używanych przez SQLAlchemy. Najpierw ustawiaSQLALCHEMY_ECHO
naTrue
. Powoduje to, że SQLAlchemy wyświetla na konsoli polecenia SQL, które wykonuje. Jest to bardzo przydatne do debugowania problemów podczas tworzenia programów bazodanowych. Ustaw to naFalse
dla środowisk produkcyjnych. -
Linia 16 ustawia
SQLALCHEMY_DATABASE_URI
dosqlite:////' + os.path.join(basedir, 'people.db')
. To mówi SQLAlchemy, aby używał SQLite jako bazy danych i pliku o nazwiepeople.db
w bieżącym katalogu jako plik bazy danych. Różne silniki baz danych, takie jak MySQL i PostgreSQL, będą miały różneSQLALCHEMY_DATABASE_URI
ciągi do ich konfiguracji. -
Linia 17 ustawia
SQLALCHEMY_TRACK_MODIFICATIONS
naFalse
, wyłączając system zdarzeń SQLAlchemy, który jest domyślnie włączony. System zdarzeń generuje zdarzenia przydatne w programach sterowanych zdarzeniami, ale powoduje znaczne obciążenie. Ponieważ nie tworzysz programu sterowanego zdarzeniami, wyłącz tę funkcję. -
Linia 19 tworzy
db
zmienna przez wywołanieSQLAlchemy(app)
. To inicjuje SQLAlchemy przez przekazanieapp
informacje o konfiguracji właśnie ustawione.db
zmienna jest importowana dobuild_database.py
program, aby dać mu dostęp do SQLAlchemy i bazy danych. Będzie służył temu samemu celowi wserver.py
program ipeople.py
moduł. -
Linia 23 tworzy
ma
zmienna, wywołującMarshmallow(app)
. To inicjuje Marshmallow i umożliwia introspekcję składników SQLAlchemy dołączonych do aplikacji. Dlatego Marshmallow jest inicjowany po SQLAlchemy.
Modele
Module
models.py
moduł jest tworzony w celu zapewnienia person
i PersonSchema
klasy dokładnie tak, jak opisano w powyższych sekcjach dotyczących modelowania i serializacji danych. Oto kod tego modułu:
1from datetime import datetime
2from config import db, ma
3
4class Person(db.Model):
5 __tablename__ = 'person'
6 person_id = db.Column(db.Integer, primary_key=True)
7 lname = db.Column(db.String(32), index=True)
8 fname = db.Column(db.String(32))
9 timestamp = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
10
11class PersonSchema(ma.ModelSchema):
12 class Meta:
13 model = Person
14 sqla_session = db.session
Oto, co robi powyższy kod:
-
Wiersz 1 importuje
datetime
obiekt zdatetime
moduł dostarczany z Pythonem. Daje to możliwość utworzenia znacznika czasu wPerson
klasa. -
Wiersz 2 importuje
db
ima
zmienne instancji zdefiniowane wconfig.py
moduł. Daje to modułowi dostęp do atrybutów i metod SQLAlchemy dołączonych dodb
zmiennej oraz atrybuty i metody Marshmallow dołączone doma
zmienna. -
Linie 4–9 zdefiniuj
person
klasy, jak omówiono w sekcji modelowania danych powyżej, ale teraz wiesz, gdzie znajduje siędb.Model
że klasa dziedziczy po źródłach. To dajePerson
klasy SQLAlchemy, takie jak połączenie z bazą danych i dostęp do jej tabel. -
Linie 11-14 zdefiniuj
PersonSchema
klasy, jak omówiono w sekcji serializacji danych powyżej. This class inherits fromma.ModelSchema
and gives thePersonSchema
class Marshmallow features, like introspecting thePerson
class to help serialize/deserialize instances of that class.
Creating the Database
You’ve seen how database tables can be mapped to SQLAlchemy classes. Now use what you’ve learned to create the database and populate it with data. You’re going to build a small utility program to create and build the database with the People
data. Here’s the build_database.py
program:
1import os
2from config import db
3from models import Person
4
5# Data to initialize database with
6PEOPLE = [
7 {'fname': 'Doug', 'lname': 'Farrell'},
8 {'fname': 'Kent', 'lname': 'Brockman'},
9 {'fname': 'Bunny','lname': 'Easter'}
10]
11
12# Delete database file if it exists currently
13if os.path.exists('people.db'):
14 os.remove('people.db')
15
16# Create the database
17db.create_all()
18
19# Iterate over the PEOPLE structure and populate the database
20for person in PEOPLE:
21 p = Person(lname=person['lname'], fname=person['fname'])
22 db.session.add(p)
23
24db.session.commit()
Here’s what the above code is doing:
-
Line 2 imports the
db
instance from theconfig.py
module. -
Line 3 imports the
Person
class definition from themodels.py
module. -
Lines 6 – 10 create the
PEOPLE
data structure, which is a list of dictionaries containing your data. The structure has been condensed to save presentation space. -
Lines 13 &14 perform some simple housekeeping to delete the
people.db
file, if it exists. This file is where the SQLite database is maintained. If you ever have to re-initialize the database to get a clean start, this makes sure you’re starting from scratch when you build the database. -
Line 17 creates the database with the
db.create_all()
call. This creates the database by using thedb
instance imported from theconfig
module.db
instance is our connection to the database. -
Lines 20 – 22 iterate over the
PEOPLE
list and use the dictionaries within to instantiate aPerson
klasa. After it is instantiated, you call thedb.session.add(p)
funkcjonować. This uses the database connection instancedb
to access thesession
obiekt. The session is what manages the database actions, which are recorded in the session. In this case, you are executing theadd(p)
method to add the newPerson
instance to thesession
obiekt. -
Line 24 calls
db.session.commit()
to actually save all the person objects created to the database.
Uwaga: At Line 22, no data has been added to the database. Everything is being saved within the session
obiekt. Only when you execute the db.session.commit()
call at Line 24 does the session interact with the database and commit the actions to it.
In SQLAlchemy, the session
is an important object. It acts as the conduit between the database and the SQLAlchemy Python objects created in a program. The session
helps maintain the consistency between data in the program and the same data as it exists in the database. It saves all database actions and will update the underlying database accordingly by both explicit and implicit actions taken by the program.
Now you’re ready to run the build_database.py
program to create and initialize the new database. You do so with the following command, with your Python virtual environment active:
python build_database.py
When the program runs, it will print SQLAlchemy log messages to the console. These are the result of setting SQLALCHEMY_ECHO
to True
in the config.py
plik. Much of what’s being logged by SQLAlchemy is the SQL
commands it’s generating to create and build the people.db
SQLite database file. Here’s an example of what’s printed out when the program is run:
2018-09-11 22:20:29,951 INFO sqlalchemy.engine.base.Engine SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
2018-09-11 22:20:29,951 INFO sqlalchemy.engine.base.Engine ()
2018-09-11 22:20:29,952 INFO sqlalchemy.engine.base.Engine SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
2018-09-11 22:20:29,952 INFO sqlalchemy.engine.base.Engine ()
2018-09-11 22:20:29,956 INFO sqlalchemy.engine.base.Engine PRAGMA table_info("person")
2018-09-11 22:20:29,956 INFO sqlalchemy.engine.base.Engine ()
2018-09-11 22:20:29,959 INFO sqlalchemy.engine.base.Engine
CREATE TABLE person (
person_id INTEGER NOT NULL,
lname VARCHAR,
fname VARCHAR,
timestamp DATETIME,
PRIMARY KEY (person_id)
)
2018-09-11 22:20:29,959 INFO sqlalchemy.engine.base.Engine ()
2018-09-11 22:20:29,975 INFO sqlalchemy.engine.base.Engine COMMIT
2018-09-11 22:20:29,980 INFO sqlalchemy.engine.base.Engine BEGIN (implicit)
2018-09-11 22:20:29,983 INFO sqlalchemy.engine.base.Engine INSERT INTO person (lname, fname, timestamp) VALUES (?, ?, ?)
2018-09-11 22:20:29,983 INFO sqlalchemy.engine.base.Engine ('Farrell', 'Doug', '2018-09-12 02:20:29.983143')
2018-09-11 22:20:29,984 INFO sqlalchemy.engine.base.Engine INSERT INTO person (lname, fname, timestamp) VALUES (?, ?, ?)
2018-09-11 22:20:29,985 INFO sqlalchemy.engine.base.Engine ('Brockman', 'Kent', '2018-09-12 02:20:29.984821')
2018-09-11 22:20:29,985 INFO sqlalchemy.engine.base.Engine INSERT INTO person (lname, fname, timestamp) VALUES (?, ?, ?)
2018-09-11 22:20:29,985 INFO sqlalchemy.engine.base.Engine ('Easter', 'Bunny', '2018-09-12 02:20:29.985462')
2018-09-11 22:20:29,986 INFO sqlalchemy.engine.base.Engine COMMIT
Using the Database
Once the database has been created, you can modify the existing code from Part 1 to make use of it. All of the modifications necessary are due to creating the person_id
primary key value in our database as the unique identifier rather than the lname
wartość.
Update the REST API
None of the changes are very dramatic, and you’ll start by re-defining the REST API. The list below shows the API definition from Part 1 but is updated to use the person_id
variable in the URL path:
Action | HTTP Verb | URL Path | Opis |
---|---|---|---|
Create | POST | /api/people | Defines a unique URL to create a new person |
Read | GET | /api/people | Defines a unique URL to read a collection of people |
Read | GET | /api/people/{person_id} | Defines a unique URL to read a particular person by person_id |
Update | PUT | /api/people/{person_id} | Defines a unique URL to update an existing person by person_id |
Delete | DELETE | /api/orders/{person_id} | Defines a unique URL to delete an existing person by person_id |
Where the URL definitions required an lname
value, they now require the person_id
(primary key) for the person record in the people
stół. This allows you to remove the code in the previous app that artificially restricted users from editing a person’s last name.
In order for you to implement these changes, the swagger.yml
file from Part 1 will have to be edited. For the most part, any lname
parameter value will be changed to person_id
, and person_id
will be added to the POST
and PUT
responses. You can check out the updated swagger.yml
file.
Update the REST API Handlers
With the swagger.yml
file updated to support the use of the person_id
identifier, you’ll also need to update the handlers in the people.py
file to support these changes. In the same way that the swagger.yml
file was updated, you need to change the people.py
file to use the person_id
value rather than lname
.
Here’s part of the updated person.py
module showing the handler for the REST URL endpoint GET /api/people
:
1from flask import (
2 make_response,
3 abort,
4)
5from config import db
6from models import (
7 Person,
8 PersonSchema,
9)
10
11def read_all():
12 """
13 This function responds to a request for /api/people
14 with the complete lists of people
15
16 :return: json string of list of people
17 """
18 # Create the list of people from our data
19 people = Person.query \
20 .order_by(Person.lname) \
21 .all()
22
23 # Serialize the data for the response
24 person_schema = PersonSchema(many=True)
25 return person_schema.dump(people).data
Here’s what the above code is doing:
-
Lines 1 – 9 import some Flask modules to create the REST API responses, as well as importing the
db
instance from theconfig.py
module. In addition, it imports the SQLAlchemyPerson
and MarshmallowPersonSchema
classes to access theperson
database table and serialize the results. -
Line 11 starts the definition of
read_all()
that responds to the REST API URL endpointGET /api/people
and returns all the records in theperson
database table sorted in ascending order by last name. -
Lines 19 – 22 tell SQLAlchemy to query the
person
database table for all the records, sort them in ascending order (the default sorting order), and return a list ofPerson
Python objects as the variablepeople
. -
Line 24 is where the Marshmallow
PersonSchema
class definition becomes valuable. You create an instance of thePersonSchema
, passing it the parametermany=True
. This tellsPersonSchema
to expect an interable to serialize, which is what thepeople
variable is. -
Line 25 uses the
PersonSchema
instance variable (person_schema
), calling itsdump()
method with thepeople
list. The result is an object having adata
attribute, an object containing apeople
list that can be converted to JSON. This is returned and converted by Connexion to JSON as the response to the REST API call.
Uwaga: The people
list variable created on Line 24 above can’t be returned directly because Connexion won’t know how to convert the timestamp
field into JSON. Returning the list of people without processing it with Marshmallow results in a long error traceback and finally this Exception:
TypeError: Object of type Person is not JSON serializable
Here’s another part of the person.py
module that makes a request for a single person from the person
Baza danych. Here, read_one(person_id)
function receives a person_id
from the REST URL path, indicating the user is looking for a specific person. Here’s part of the updated person.py
module showing the handler for the REST URL endpoint GET /api/people/{person_id}
:
1def read_one(person_id):
2 """
3 This function responds to a request for /api/people/{person_id}
4 with one matching person from people
5
6 :param person_id: ID of person to find
7 :return: person matching ID
8 """
9 # Get the person requested
10 person = Person.query \
11 .filter(Person.person_id == person_id) \
12 .one_or_none()
13
14 # Did we find a person?
15 if person is not None:
16
17 # Serialize the data for the response
18 person_schema = PersonSchema()
19 return person_schema.dump(person).data
20
21 # Otherwise, nope, didn't find that person
22 else:
23 abort(404, 'Person not found for Id: {person_id}'.format(person_id=person_id))
Here’s what the above code is doing:
-
Lines 10 – 12 use the
person_id
parameter in a SQLAlchemy query using thefilter
method of the query object to search for a person with aperson_id
attribute matching the passed-inperson_id
. Rather than using theall()
query method, use theone_or_none()
method to get one person, or returnNone
if no match is found. -
Line 15 determines whether a
person
was found or not. -
Line 17 shows that, if
person
was notNone
(a matchingperson
was found), then serializing the data is a little different. You don’t pass themany=True
parameter to the creation of thePersonSchema()
instance. Instead, you passmany=False
because only a single object is passed in to serialize. -
Line 18 is where the
dump
method ofperson_schema
is called, and thedata
attribute of the resulting object is returned. -
Line 23 shows that, if
person
wasNone
(a matching person wasn’t found), then the Flaskabort()
method is called to return an error.
Another modification to person.py
is creating a new person in the database. This gives you an opportunity to use the Marshmallow PersonSchema
to deserialize a JSON structure sent with the HTTP request to create a SQLAlchemy Person
obiekt. Here’s part of the updated person.py
module showing the handler for the REST URL endpoint POST /api/people
:
1def create(person):
2 """
3 This function creates a new person in the people structure
4 based on the passed-in person data
5
6 :param person: person to create in people structure
7 :return: 201 on success, 406 on person exists
8 """
9 fname = person.get('fname')
10 lname = person.get('lname')
11
12 existing_person = Person.query \
13 .filter(Person.fname == fname) \
14 .filter(Person.lname == lname) \
15 .one_or_none()
16
17 # Can we insert this person?
18 if existing_person is None:
19
20 # Create a person instance using the schema and the passed-in person
21 schema = PersonSchema()
22 new_person = schema.load(person, session=db.session).data
23
24 # Add the person to the database
25 db.session.add(new_person)
26 db.session.commit()
27
28 # Serialize and return the newly created person in the response
29 return schema.dump(new_person).data, 201
30
31 # Otherwise, nope, person exists already
32 else:
33 abort(409, f'Person {fname} {lname} exists already')
Here’s what the above code is doing:
-
Line 9 &10 set the
fname
andlname
variables based on thePerson
data structure sent as thePOST
body of the HTTP request. -
Lines 12 – 15 use the SQLAlchemy
Person
class to query the database for the existence of a person with the samefname
andlname
as the passed-inperson
. -
Line 18 addresses whether
existing_person
isNone
. (existing_person
was not found.) -
Line 21 creates a
PersonSchema()
instance calledschema
. -
Line 22 uses the
schema
variable to load the data contained in theperson
parameter variable and create a new SQLAlchemyPerson
instance variable callednew_person
. -
Line 25 adds the
new_person
instance to thedb.session
. -
Line 26 commits the
new_person
instance to the database, which also assigns it a new primary key value (based on the auto-incrementing integer) and a UTC-based timestamp. -
Line 33 shows that, if
existing_person
is notNone
(a matching person was found), then the Flaskabort()
method is called to return an error.
Update the Swagger UI
With the above changes in place, your REST API is now functional. The changes you’ve made are also reflected in an updated swagger UI interface and can be interacted with in the same manner. Below is a screenshot of the updated swagger UI opened to the GET /people/{person_id}
Sekcja. This section of the UI gets a single person from the database and looks like this:
As shown in the above screenshot, the path parameter lname
has been replaced by person_id
, which is the primary key for a person in the REST API. The changes to the UI are a combined result of changing the swagger.yml
file and the code changes made to support that.
Update the Web Application
The REST API is running, and CRUD operations are being persisted to the database. So that it is possible to view the demonstration web application, the JavaScript code has to be updated.
The updates are again related to using person_id
instead of lname
as the primary key for person data. In addition, the person_id
is attached to the rows of the display table as HTML data attributes named data-person-id
, so the value can be retrieved and used by the JavaScript code.
This article focused on the database and making your REST API use it, which is why there’s just a link to the updated JavaScript source and not much discussion of what it does.
Example Code
All of the example code for this article is available here. There’s one version of the code containing all the files, including the build_database.py
utility program and the server.py
modified example program from Part 1.
Wniosek
Congratulations, you’ve covered a lot of new material in this article and added useful tools to your arsenal!
You’ve learned how to save Python objects to a database using SQLAlchemy. You’ve also learned how to use Marshmallow to serialize and deserialize SQLAlchemy objects and use them with a JSON REST API. The things you’ve learned have certainly been a step up in complexity from the simple REST API of Part 1, but that step has given you two very powerful tools to use when creating more complex applications.
SQLAlchemy and Marshmallow are amazing tools in their own right. Using them together gives you a great leg up to create your own web applications backed by a database.
In Part 3 of this series, you’ll focus on the R
part of RDBMS
:relationships, which provide even more power when you are using a database.