В част 1 от тази серия използвахте Flask и Connexion, за да създадете REST API, осигуряващ CRUD операции към проста структура в паметта, наречена PEOPLE
. Това работи, за да демонстрира как модулът Connexion ви помага да изградите приятен REST API заедно с интерактивна документация.
Както някои отбелязаха в коментарите за част 1, PEOPLE
структурата се инициализира отново всеки път, когато приложението се рестартира. В тази статия ще научите как да съхранявате PEOPLE
структура и действията, които API предоставя, към база данни с помощта на SQLAlchemy и Marshmallow.
SQLAlchemy предоставя обектен релационен модел (ORM), който съхранява обекти на Python в представяне на база данни на данните на обекта. Това може да ви помогне да продължите да мислите по Pythonic начин и да не се занимавате с това как данните за обекта ще бъдат представени в база данни.
Marshmallow предоставя функционалност за сериализиране и десериализиране на Python обекти, докато те изтичат от и в нашия базиран на JSON REST API. Marshmallow преобразува екземпляри на клас Python в обекти, които могат да бъдат преобразувани в JSON.
Можете да намерите кода на Python за тази статия тук.
Безплатен бонус: Щракнете тук, за да изтеглите копие от ръководството „Примери за REST API“ и да получите практическо въведение в принципите на Python + REST API с примери за действие.
За кого е тази статия
Ако ви е харесала част 1 от тази поредица, тази статия разширява колана ви с инструменти още повече. Ще използвате SQLAlchemy за достъп до база данни по по-питоничен начин от директния SQL. Също така ще използвате Marshmallow за сериализиране и десериализиране на данните, управлявани от REST API. За да направите това, ще използвате основните функции за обектно-ориентирано програмиране, налични в Python.
Също така ще използвате SQLAlchemy за създаване на база данни, както и за взаимодействие с нея. Това е необходимо, за да стартирате REST API и да работи с PEOPLE
данни, използвани в част 1.
Уеб приложението, представено в част 1, ще има своите HTML и JavaScript файлове, променени по незначителни начини, за да поддържа и промените. Можете да прегледате окончателната версия на кода от част 1 тук.
Допълнителни зависимости
Преди да започнете да изграждате тази нова функционалност, ще трябва да актуализирате virtualenv, който сте създали, за да стартирате кода на част 1, или да създадете нов за този проект. Най-лесният начин да направите това, след като сте активирали своя virtualenv, е да изпълните тази команда:
$ pip install Flask-SQLAlchemy flask-marshmallow marshmallow-sqlalchemy marshmallow
Това добавя повече функционалност към вашия virtualenv:
-
Flask-SQLAlchemy
добавя SQLAlchemy, заедно с някои връзки към Flask, позволявайки на програмите да имат достъп до бази данни. -
flask-marshmallow
добавя Flask частите на Marshmallow, което позволява на програмите да конвертират Python обекти към и от сериализиращи се структури. -
marshmallow-sqlalchemy
добавя някои куки Marshmallow в SQLAlchemy, за да позволи на програмите да сериализират и десериализират Python обекти, генерирани от SQLAlchemy. -
marshmallow
добавя по-голямата част от функционалността на Marshmallow.
Данни за хората
Както бе споменато по-горе, PEOPLE
структурата на данните в предишната статия е речник на Python в паметта. В този речник сте използвали фамилното име на лицето като ключ за търсене. Структурата на данните изглеждаше така в кода:
# 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()
}
}
Промените, които ще направите в програмата, ще преместят всички данни в таблица на база данни. Това означава, че данните ще бъдат запазени на вашия диск и ще съществуват между стартиранията на server.py
програма.
Тъй като фамилното име беше ключът на речника, кодът ограничаваше промяната на фамилното име на човек:можеше да се промени само първото име. Освен това преместването към база данни ще ви позволи да промените фамилното име, тъй като то вече няма да се използва като ключ за търсене на човек.
Концептуално таблицата на базата данни може да се разглежда като двуизмерен масив, където редовете са записи, а колоните са полета в тези записи.
Таблиците на базата данни обикновено имат автоматично увеличаваща се целочислена стойност като ключ за търсене на редове. Това се нарича първичен ключ. Всеки запис в таблицата ще има първичен ключ, чиято стойност е уникална в цялата таблица. Наличието на първичен ключ, независим от данните, съхранявани в таблицата, ви освобождава да променяте всяко друго поле в реда.
Забележка:
Автоматично увеличаващият се първичен ключ означава, че базата данни се грижи за:
- Увеличаване на най-голямото съществуващо поле за първичен ключ всеки път, когато в таблицата се вмъкне нов запис
- Използване на тази стойност като първичен ключ за нововмъкнатите данни
Това гарантира уникален първичен ключ с нарастването на таблицата.
Ще следвате конвенцията на базата данни за именуване на таблицата като единствено число, така че таблицата ще се нарича person
. Превеждаме нашите PEOPLE
структура по-горе в таблица на база данни с име person
ви дава това:
person_id | име | fname | timestamp |
---|---|---|---|
1 | Фарел | Дъг | 2018-08-08 21:16:01.888444 |
2 | Брокман | Кент | 2018-08-08 21:16:01.889060 |
3 | Великден | Зайче | 2018-08-08 21:16:01.886834 |
Всяка колона в таблицата има име на поле, както следва:
person_id
: поле за първичен ключ за всяко лицеlname
: фамилно име на лицетоfname
: собствено име на лицетоtimestamp
: клеймо за време, свързано с действия за вмъкване/актуализация
Взаимодействие с базата данни
Ще използвате SQLite като машина за база данни за съхраняване на PEOPLE
данни. SQLite е най-широко разпространената база данни в света и се предлага с Python безплатно. Той е бърз, изпълнява цялата си работа с файлове и е подходящ за много проекти. Това е цялостна RDBMS (система за управление на релационни бази данни), която включва SQL, езикът на много системи за бази данни.
За момента си представете person
таблицата вече съществува в база данни на SQLite. Ако сте имали някакъв опит с RDBMS, вероятно сте запознати с SQL, езикът за структурирани заявки, който повечето RDBMS използват за взаимодействие с базата данни.
За разлика от езиците за програмиране като Python, SQL не дефинира как за да получите данните:описва какво данните са желани, оставяйки как до двигателя на базата данни.
SQL заявка, която получава всички данни в нашия person
таблицата, сортирана по фамилно име, ще изглежда така:
SELECT * FROM person ORDER BY 'lname';
Тази заявка казва на двигателя на базата данни да получи всички полета от таблицата с хора и да ги сортира във възходящ ред по подразбиране с помощта на lname
поле.
Ако трябва да изпълните тази заявка срещу SQLite база данни, съдържаща person
таблица, резултатите ще бъдат набор от записи, съдържащи всички редове в таблицата, като всеки ред съдържа данните от всички полета, съставляващи един ред. По-долу е даден пример за използване на инструмента за команден ред SQLite, изпълняващ горната заявка срещу person
таблица на базата данни:
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
Резултатът по-горе е списък на всички редове в person
таблица на базата данни със символи (‘|’), разделящи полетата в реда, което се прави за целите на показване от SQLite.
Python е напълно способен да взаимодейства с много машини за бази данни и да изпълнява SQL заявката по-горе. Резултатите най-вероятно ще бъдат списък с кортежи. Външният списък съдържа всички записи в person
маса. Всеки отделен вътрешен кортеж ще съдържа всички данни, представляващи всяко поле, дефинирано за ред на таблица.
Получаването на данни по този начин не е много Pythonic. Списъкът със записи е наред, но всеки отделен запис е просто набор от данни. Програмата трябва да знае индекса на всяко поле, за да извлече конкретно поле. Следният код на Python използва SQLite, за да демонстрира как да изпълните горната заявка и да покаже данните:
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]}')
Програмата по-горе прави следното:
-
Линия 1 импортира
sqlite3
модул. -
Ред 3 създава връзка с файла на базата данни.
-
Реда 4 създава курсор от връзката.
-
Ред 5 използва курсора, за да изпълни
SQL
заявка, изразена като низ. -
Ред 6 получава всички записи, върнати от
SQL
заявка и ги присвоява наpeople
променлива. -
Ред 7 и 8 итерирайте над
people
списък на променлива и отпечатайте името и фамилията на всеки човек.
people
променлива от ред 6 по-горе ще изглежда така в Python:
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')
]
Резултатът от програмата по-горе изглежда така:
Kent Brockman
Bunny Easter
Doug Farrell
В горната програма трябва да знаете, че името на човек е в индекс 2
, а фамилното име на човек е в индекс 1
. Още по-лошо, вътрешната структура на person
трябва също да се знае всеки път, когато предавате итерационната променлива person
като параметър на функция или метод.
Би било много по-добре това, което сте получили обратно за person
беше обект на Python, където всяко от полетата е атрибут на обекта. Това е едно от нещата, които SQLAlchemy прави.
Масички Боби
В горната програма SQL операторът е прост низ, предаван директно на базата данни за изпълнение. В този случай това не е проблем, защото SQL е низов литерал, изцяло под контрола на програмата. Въпреки това, случаят на използване на вашия REST API ще вземе потребителски вход от уеб приложението и ще го използва за създаване на SQL заявки. Това може да отвори приложението ви за атака.
Ще си спомните от част 1, че REST API за получаване на един person
от PEOPLE
данните изглеждаха така:
GET /api/people/{lname}
Това означава, че вашият API очаква променлива, lname
, в пътя на крайната точка на URL адреса, който използва за намиране на един person
. Промяната на кода на Python SQLite отгоре, за да се направи това, би изглеждала така:
1lname = 'Farrell'
2cur.execute('SELECT * FROM person WHERE lname = \'{}\''.format(lname))
Горният кодов фрагмент прави следното:
-
Линия 1 задава
lname
променлива до'Farrell'
. Това ще дойде от пътя на крайната точка на URL адреса на REST API. -
Ред 2 използва форматиране на низове на Python, за да създаде SQL низ и да го изпълни.
За да бъде нещата опростени, горният код задава lname
променлива към константа, но наистина тя би идвала от пътя на крайната точка на URL адреса на API и може да бъде всичко, предоставено от потребителя. SQL, генериран от форматирането на низа, изглежда така:
SELECT * FROM person WHERE lname = 'Farrell'
Когато този SQL се изпълнява от базата данни, той търси person
таблица за запис, където фамилното име е равно на 'Farrell'
. Това е, което е предназначено, но всяка програма, която приема въвеждане на потребител, е отворена и за злонамерени потребители. В програмата по-горе, където lname
променливата се задава от предоставено от потребителя въвеждане, това отваря вашата програма за това, което се нарича атака с инжектиране на SQL. Това е това, което нежно е известно като Little Bobby Tables:
Например, представете си злонамерен потребител, наречен вашия REST API по този начин:
GET /api/people/Farrell');DROP TABLE person;
Заявката за REST API по-горе задава lname
променлива към 'Farrell');DROP TABLE person;'
, който в кода по-горе ще генерира този SQL израз:
SELECT * FROM person WHERE lname = 'Farrell');DROP TABLE person;
Горният SQL оператор е валиден и когато се изпълни от базата данни, той ще намери един запис, където lname
съвпада с 'Farrell'
. След това ще намери разделителя на SQL израза ;
и ще продължи напред и ще пусне цялата маса. Това по същество би разрушило приложението ви.
Можете да защитите програмата си, като дезинфекцирате всички данни, които получавате от потребителите на вашето приложение. Дезинфекцията на данните в този контекст означава, че програмата ви проверява предоставените от потребителя данни и се уверява, че не съдържат нищо опасно за програмата. Това може да бъде трудно да се направи правилно и ще трябва да се прави навсякъде, където потребителските данни взаимодействат с базата данни.
Има друг начин, който е много по-лесен:използвайте SQLAlchemy. Той ще дезинфекцира потребителските данни вместо вас, преди да създаде SQL изрази. Това е друго голямо предимство и причина да използвате SQLAlchemy при работа с бази данни.
Моделиране на данни с SQLAlchemy
SQLAlchemy е голям проект и предоставя много функционалност за работа с бази данни с помощта на Python. Едно от нещата, които предоставя, е ORM или Object Relational Mapper и това е, което ще използвате, за да създадете и работите с person
таблица на базата данни. Това ви позволява да картографирате ред полета от таблицата на базата данни към обект на Python.
Обектно ориентираното програмиране ви позволява да свържете данни заедно с поведението, функциите, които оперират с тези данни. Чрез създаване на класове SQLAlchemy вие можете да свържете полетата от редовете на таблицата на базата данни с поведението, което ви позволява да взаимодействате с данните. Ето дефиницията на клас SQLAlchemy за данните в person
таблица на базата данни:
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)
Класът Person
наследява от db.Model
, до който ще стигнете, когато започнете да създавате програмния код. Засега това означава, че наследявате от базов клас, наречен Model
, предоставящ атрибути и функционалност, общи за всички класове, получени от него.
Останалите дефиниции са атрибути на ниво клас, дефинирани както следва:
-
__tablename__ = 'person'
свързва дефиницията на класа сperson
таблица на базата данни. -
person_id = db.Column(db.Integer, primary_key=True)
създава колона на база данни, съдържаща цяло число, действащо като първичен ключ за таблицата. Това също така казва на базата данни, чеperson_id
ще бъде автоматично увеличаваща се целочислена стойност. -
lname = db.Column(db.String)
създава полето за фамилно име, колона на базата данни, съдържаща стойност на низ. -
fname = db.Column(db.String)
създава първото поле за име, колона на базата данни, съдържаща стойност на низ. -
timestamp = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
създава поле за времеви отпечатък, колона на базата данни, съдържаща стойност за дата/час.default=datetime.utcnow
параметърът по подразбиране задава стойността на времевата марка на текущияutcnow
стойност, когато е създаден запис.onupdate=datetime.utcnow
параметър актуализира времевата марка с текущияutcnow
стойност, когато записът се актуализира.
Забележка:UTC маркировки за време
Може би се чудите защо времевата марка в горния клас по подразбиране е и се актуализира от datetime.utcnow()
метод, който връща UTC или координирано универсално време. Това е начин за стандартизиране на източника на вашата времева марка.
Източникът или нулево време е линия, минаваща на север и юг от северния до южния полюс на Земята през Обединеното кралство. Това е нулевата часова зона, от която всички останали часови зони са изместени. Като използвате това като нулев източник на време, вашите времеви марки са отместени спрямо тази стандартна референтна точка.
Ако приложението ви бъде достъпно от различни часови зони, имате начин да извършвате изчисления за дата/час. Всичко, от което се нуждаете, е времева марка по UTC и часова зона на местоназначението.
Ако трябваше да използвате местните часови зони като източник на времеви отпечатъци, тогава не бихте могли да извършвате изчисления за дата/час без информация за местните часови зони, изместени от нулево време. Без информацията за източника на времевата марка не бихте могли да правите никакви сравнения на дата/час или математика.
Работата с времеви марки, базирани на UTC, е добър стандарт, който трябва да се следва. Ето един сайт с инструменти, с който да работите и да ги разберете по-добре.
Накъде се насочвате с това Person
дефиниция на класа? Крайната цел е да можете да изпълните заявка с помощта на SQLAlchemy и да получите обратно списък с екземпляри на Person
клас. Като пример, нека разгледаме предишния SQL израз:
SELECT * FROM people ORDER BY lname;
Покажете същата малка примерна програма отгоре, но сега с помощта на 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}')
Като игнорирате ред 1 за момента, това, което искате, е целият person
записи, сортирани във възходящ ред по lname
поле. Какво получавате от операторите на SQLAlchemy Person.query.order_by(Person.lname).all()
е списък на Person
обекти за всички записи в person
таблица на базата данни в този ред. В горната програма people
променливата съдържа списъка на Person
обекти.
Програмата преглежда people
променлива, като всеки person
на свой ред и отпечатване на името и фамилията на лицето от базата данни. Забележете, че програмата не трябва да използва индекси, за да получи fname
или lname
стойности:използва атрибутите, дефинирани в Person
обект.
Използването на SQLAlchemy ви позволява да мислите по отношение на обекти с поведение, а не суров SQL
. Това става още по-полезно, когато таблиците на вашата база данни станат по-големи и взаимодействията по-сложни.
Сериализиране/десериализиране на моделирани данни
Работата с SQLAlchemy моделирани данни във вашите програми е много удобна. Това е особено удобно в програми, които манипулират данните, може би правят изчисления или ги използват за създаване на презентации на екрана. Приложението ви е REST API, което по същество осигурява CRUD операции върху данните и като такова не извършва много манипулации на данни.
REST API работи с JSON данни и тук можете да срещнете проблем с модела SQLAlchemy. Тъй като данните, върнати от SQLAlchemy, са екземпляри на клас Python, Connexion не може да сериализира тези екземпляри на клас в JSON форматирани данни. Не забравяйте от част 1, че Connexion е инструментът, който сте използвали за проектиране и конфигуриране на REST API с помощта на YAML файл и свързване на методите на Python към него.
В този контекст сериализирането означава преобразуване на Python обекти, които могат да съдържат други обекти на Python и сложни типове данни, в по-прости структури от данни, които могат да бъдат анализирани в JSON типове данни, които са изброени тук:
string
: тип низnumber
: числа, поддържани от Python (цели числа, плаващи числа, дълги)object
: JSON обект, който е приблизително еквивалентен на речник на Pythonarray
: приблизително еквивалентен на списък на Pythonboolean
: представено в JSON катоtrue
илиfalse
, но в Python катоTrue
илиFalse
null
: по съществоNone
в Python
Като пример, вашето Person
клас съдържа времева марка, която е DateTime
на Python . В JSON няма дефиниция за дата/час, така че клеймото за време трябва да бъде преобразувано в низ, за да съществува в JSON структура.
Вашето Person
class е достатъчно прост, така че получаването на атрибутите на данните от него и ръчното създаване на речник, който да се върне от нашите крайни точки на REST URL, няма да бъде много трудно. В по-сложно приложение с много по-големи модели на SQLAlchemy това няма да е така. По-добро решение е да използвате модул, наречен Marshmallow, за да свърши работата вместо вас.
Marshmallow ви помага да създадете PersonSchema
клас, който е като SQLAlchemy Person
клас, който създадохме. Тук обаче, вместо да съпоставя таблици на база данни и имена на полета към класа и неговите атрибути, PersonSchema
class определя как атрибутите на клас ще бъдат преобразувани в удобни за JSON формати. Ето дефиницията на класа Marshmallow за данните в нашия person
таблица:
class PersonSchema(ma.ModelSchema):
class Meta:
model = Person
sqla_session = db.session
Класът PersonSchema
наследява от ma.ModelSchema
, до който ще стигнете, когато започнете да създавате програмния код. Засега това означава PersonSchema
наследява от базов клас Marshmallow, наречен ModelSchema
, предоставящ атрибути и функционалност, общи за всички класове, получени от него.
Останалата част от определението е както следва:
-
class Meta
дефинира клас с имеMeta
в рамките на вашия клас.ModelSchema
клас, койтоPersonSchema
класът наследява от външния вид на този вътрешенMeta
клас и го използва за намиране на SQLAlchemy моделPerson
иdb.session
. Ето как Marshmallow намира атрибути вPerson
клас и типа на тези атрибути, така че да знае как да ги сериализира/десериализира. -
model
казва на класа какъв SQLAlchemy модел да използва за сериализиране/десериализиране на данни към и от. -
db.session
казва на класа коя сесия на базата данни да използва за интроспекция и определяне на типове данни за атрибути.
Накъде се насочвате с тази дефиниция на клас? Искате да можете да сериализирате екземпляр на Person
клас в JSON данни и за десериализиране на JSON данни и създаване на Person
екземпляри на клас от него.
Създайте инициализираната база данни
SQLAlchemy обработва много от взаимодействията, специфични за конкретни бази данни и ви позволява да се съсредоточите върху моделите на данни, както и как да ги използвате.
Сега, когато всъщност ще създадете база данни, както беше споменато по-горе, ще използвате SQLite. Правите това по няколко причини. Той идва с Python и не е необходимо да се инсталира като отделен модул. Той записва цялата информация за базата данни в един файл и следователно е лесен за настройка и използване.
Инсталирането на отделен сървър на база данни като MySQL или PostgreSQL би работило добре, но ще изисква инсталиране на тези системи и тяхното стартиране, което е извън обхвата на тази статия.
Тъй като SQLAlchemy обработва базата данни, в много отношения наистина няма значение каква е основната база данни.
Ще създадете нова помощна програма, наречена build_database.py
за създаване и инициализиране на SQLite people.db
файл с база данни, съдържащ вашето person
таблица на базата данни. По пътя ще създадете два модула на Python, config.py
и models.py
, който ще се използва от build_database.py
и модифицираният server.py
от част 1.
Ето къде можете да намерите изходния код за модулите, които предстои да създадете, които са представени тук:
-
config.py
получава необходимите модули, импортирани в програмата и конфигурирани. Това включва Flask, Connexion, SQLAlchemy и Marshmallow. Тъй като ще се използва както отbuild_database.py
иserver.py
, някои части от конфигурацията ще се прилагат само заserver.py
приложение. -
models.py
е модулът, в който ще създадетеPerson
SQLAlchemy иPersonSchema
Дефинициите на клас Marshmallow, описани по-горе. Този модул зависи отconfig.py
за някои от обектите, създадени и конфигурирани там.
Конфигурационен модул
config.py
модул, както подсказва името, е мястото, където се създава и инициализира цялата информация за конфигурацията. Ще използваме този модул както за нашия build_database.py
програмен файл и скоро ще бъде актуализиран server.py
файл от част 1 статия. Това означава, че тук ще конфигурираме Flask, Connexion, SQLAlchemy и Marshmallow.
Въпреки че build_database.py
програмата не използва Flask, Connexion или Marshmallow, тя използва SQLAlchemy, за да създаде нашата връзка с базата данни на SQLite. Ето кода за config.py
модул:
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)
Ето какво прави горният код:
-
Редове 2 – 4 импортирайте Connexion, както направихте в
server.py
програма от част 1. Освен това импортираSQLAlchemy
отflask_sqlalchemy
модул. Това дава на вашата програма достъп до базата данни. И накрая, импортираMarshmallow
отflask_marshamllow
модул. -
Ред 6 създава променливата
basedir
сочещи към директорията, в която се изпълнява програмата. -
Линия 9 използва
basedir
променлива, за да създадете екземпляра на приложението Connexion и да му дадете пътя къмswagger.yml
файл. -
Ред 12 създава променлива
app
, което е екземплярът на Flask, инициализиран от Connexion. -
Редове 15 използва
app
променлива за конфигуриране на стойности, използвани от SQLAlchemy. Първо задаваSQLALCHEMY_ECHO
доTrue
. Това кара SQLAlchemy да повтаря SQL изрази, които изпълнява, към конзолата. Това е много полезно за отстраняване на грешки при изграждане на програми за бази данни. Задайте това наFalse
за производствени среди. -
Линия 16 задава
SQLALCHEMY_DATABASE_URI
къмsqlite:////' + os.path.join(basedir, 'people.db')
. Това казва на SQLAlchemy да използва SQLite като база данни и файл с имеpeople.db
в текущата директория като файл на базата данни. Различните машини за бази данни, като MySQL и PostgreSQL, ще имат различенSQLALCHEMY_DATABASE_URI
низове, за да ги конфигурирате. -
Линия 17 задава
SQLALCHEMY_TRACK_MODIFICATIONS
доFalse
, изключване на системата за събития SQLAlchemy, която е включена по подразбиране. Системата за събития генерира събития, полезни в програми, управлявани от събития, но добавя значителни допълнителни разходи. Тъй като не създавате програма, управлявана от събития, изключете тази функция. -
Линия 19 създава
db
променлива чрез извикване наSQLAlchemy(app)
. Това инициализира SQLAlchemy чрез предаване наapp
току-що зададена информация за конфигурацията.db
променливата е това, което се импортира вbuild_database.py
програма, за да му даде достъп до SQLAlchemy и базата данни. Той ще служи за същата цел вserver.py
програма иpeople.py
модул. -
Ред 23 създава
ma
променлива чрез извикване наMarshmallow(app)
. Това инициализира Marshmallow и му позволява да интроспектира компонентите на SQLAlchemy, прикачени към приложението. Ето защо Marshmallow се инициализира след SQLAlchemy.
Модул за модели
models.py
модулът е създаден, за да предостави Person
и PersonSchema
класове точно както е описано в разделите по-горе относно моделирането и сериализирането на данните. Ето кода за този модул:
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
Ето какво прави горният код:
-
Линия 1 импортира
datetime
обект отdatetime
модул, който идва с Python. Това ви дава начин да създадете времева марка вPerson
клас. -
Ред 2 импортира
db
иma
променливи на екземпляра, дефинирани вconfig.py
модул. Това дава на модула достъп до атрибути и методи на SQLAlchemy, прикачени къмdb
променлива и атрибутите и методите на Marshmallow, прикачени къмma
променлива. -
Редове 4 – 9 дефинирайте
Person
клас, както беше обсъдено в раздела за моделиране на данни по-горе, но сега знаете къде се намираdb.Model
че класът наследява от origins. Това даваPerson
функции на клас SQLAlchemy, като връзка с базата данни и достъп до нейните таблици. -
Редове 11 – 14 дефинирайте
PersonSchema
клас, както беше обсъдено в раздела за сериализиране на данни по-горе. Този клас наследява отma.ModelSchema
и даваPersonSchema
функции на клас Marshmallow, като интроспекция наPerson
клас, за да помогне за сериализирането/десериализирането на екземпляри от този клас.
Създаване на базата данни
Видяхте как таблиците на базата данни могат да бъдат съпоставени с SQLAlchemy класове. Сега използвайте това, което сте научили, за да създадете база данни и да я попълните с данни. Ще създадете малка помощна програма за създаване и изграждане на база данни с people
данни. 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:
-
Ред 2 imports the
db
instance from theconfig.py
module. -
Ред 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()
обадете се. This creates the database by using thedb
instance imported from theconfig
модул. Thedb
instance is our connection to the database. -
Lines 20 – 22 повторете над
PEOPLE
list and use the dictionaries within to instantiate aPerson
клас. After it is instantiated, you call thedb.session.add(p)
функция. This uses the database connection instancedb
to access thesession
обект. 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
обект. -
Line 24 calls
db.session.commit()
to actually save all the person objects created to the database.
Забележка: At Line 22, no data has been added to the database. Everything is being saved within the session
обект. 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
файл. 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
value.
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:
Действие | HTTP глагол | URL път | Описание |
---|---|---|---|
Създаване | POST | /api/people | Defines a unique URL to create a new person |
Прочетете | GET | /api/people | Defines a unique URL to read a collection of people |
Прочетете | GET | /api/people/{person_id} | Defines a unique URL to read a particular person by person_id |
Актуализиране | PUT | /api/people/{person_id} | Defines a unique URL to update an existing person by person_id |
Изтриване | 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
маса. 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
отговори. 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
модул. 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
списък. 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.
Забележка: 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
база данни. 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()
екземпляр. 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
обект. 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
еNone
. (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}
section. 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.
Заключение
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.