Database
 sql >> база данни >  >> RDS >> Database

Python REST API с Flask, Connexion и SQLAlchemy – част 2

В част 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:

  1. Flask-SQLAlchemy добавя SQLAlchemy, заедно с някои връзки към Flask, позволявайки на програмите да имат достъп до бази данни.

  2. flask-marshmallow добавя Flask частите на Marshmallow, което позволява на програмите да конвертират Python обекти към и от сериализиращи се структури.

  3. marshmallow-sqlalchemy добавя някои куки Marshmallow в SQLAlchemy, за да позволи на програмите да сериализират и десериализират Python обекти, генерирани от SQLAlchemy.

  4. 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 обект, който е приблизително еквивалентен на речник на Python
  • array : приблизително еквивалентен на списък на Python
  • boolean : представено в 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 the config.py module.

  • Ред 3 imports the Person class definition from the models.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 the db instance imported from the config модул. The db instance is our connection to the database.

  • Lines 20 – 22 повторете над PEOPLE list and use the dictionaries within to instantiate a Person клас. After it is instantiated, you call the db.session.add(p) функция. This uses the database connection instance db to access the session обект. The session is what manages the database actions, which are recorded in the session. In this case, you are executing the add(p) method to add the new Person instance to the session обект.

  • 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 the config.py модул. In addition, it imports the SQLAlchemy Person and Marshmallow PersonSchema classes to access the person database table and serialize the results.

  • Line 11 starts the definition of read_all() that responds to the REST API URL endpoint GET /api/people and returns all the records in the person 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 of Person Python objects as the variable people .

  • Line 24 is where the Marshmallow PersonSchema class definition becomes valuable. You create an instance of the PersonSchema , passing it the parameter many=True . This tells PersonSchema to expect an interable to serialize, which is what the people variable is.

  • Line 25 uses the PersonSchema instance variable (person_schema ), calling its dump() method with the people списък. The result is an object having a data attribute, an object containing a people 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 the filter method of the query object to search for a person with a person_id attribute matching the passed-in person_id . Rather than using the all() query method, use the one_or_none() method to get one person, or return None if no match is found.

  • Line 15 determines whether a person was found or not.

  • Line 17 shows that, if person was not None (a matching person was found), then serializing the data is a little different. You don’t pass the many=True parameter to the creation of the PersonSchema() екземпляр. Instead, you pass many=False because only a single object is passed in to serialize.

  • Line 18 is where the dump method of person_schema is called, and the data attribute of the resulting object is returned.

  • Line 23 shows that, if person was None (a matching person wasn’t found), then the Flask abort() 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 and lname variables based on the Person data structure sent as the POST 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 same fname and lname as the passed-in person .

  • Line 18 addresses whether existing_person е None . (existing_person was not found.)

  • Line 21 creates a PersonSchema() instance called schema .

  • Line 22 uses the schema variable to load the data contained in the person parameter variable and create a new SQLAlchemy Person instance variable called new_person .

  • Line 25 adds the new_person instance to the db.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 not None (a matching person was found), then the Flask abort() 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.

« Part 1:REST APIs With Flask + ConnexionPart 2:Database PersistencePart 3:Database Relationships »

  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Основи на табличните изрази, част 1

  2. MuleSoft прегръща GraphQL за усъвършенстване на интеграцията на API

  3. SQL BETWEEN оператор за начинаещи

  4. Как да извлечем набор от знаци с помощта на SUBSTRING в SQL?

  5. Как AI ще промени разработката и тестването на софтуер