Это перевод статьи от Philipp Acsany
Большинство современных веб-приложений работают на основе REST API - методологии, позволяющей разработчикам отделить разработку пользовательского интерфейса (FrontEnd) от разработки внутренней серверной логики (BackEnd), а пользователи получают интерфейс с динамически подгружаемыми данными. В этой серии из трех частей вы создадите REST API с помощью веб-фреймворка Flask.
В первой части и второй части руководства вы создали базовый проект Flask и добавили конечные точки, которые вы подключили к базе данных SQLite, используя сериализацию и десериализацию JSON в объекты Python с помощью Marshmallow. Вы также дополнили свой API новыми возможностями, для которых создали аннотации с помощью Swagger UI API.
В этой третьей части серии вы узнаете, как:
- Работать с несколькими таблицами с взаимосвязанной информацией в базе данных
- Создавать связи «один ко многим» в базе данных
- Управлять связями с помощью SQLAlchemy
- Сериализовать сложные схемы данных со связями с помощью Marshmallow
- Отображать связанные объекты в клиентском интерфейсе
Техзадание
Создать приложение для управления открытками персонажам, от которых вы можете получить подарки в течение года. Вот эти сказочные лица: Tooth Fairy (Зубная фея), Easter Bunny (Пасхальный кролик) и Knecht Ruprecht (Кнехт Рупрехт).
В идеале вы хотите быть в хороших отношениях со всеми тремя из них, вот почему вы будете отправлять им открытки, чтобы увеличить шансы получить от них ценные подарки.
В этой третьей части вы еще больше расширите свой инструментарий программирования. Вы узнаете, как создавать иерархические структуры данных, представленные в виде отношений «один ко многим» с помощью SQLAlchemy. Кроме того, вы также расширите API REST, который вы уже создали, для создания, чтения, обновления и удаления открыток для персонажей.
Пришло время завершить это руководство из трех частей, создав отношения между персонажами и открытками!
План части 3
В первой части вы начали создавать REST API с хранением данных в словаре PEOPLE
. Затем, во второй части вы реализовали подключение REST API к базе данных для сохранения данных в промежутках между запусками приложения.
Вы добавили возможность сохранять изменения, внесенные через REST API, в базу данных с помощью SQLAlchemy и узнали, как сериализовать эти данные для REST API с помощью Marshmallow.
В настоящее время база данных people.db
содержит только данные о персонажах. В этой части серии вы добавите новую таблицу для хранения открыток. Чтобы связать открытки с персонажем, которому они предназначены, вы создадите связи между записями таблицы person
и таблицы note
в базе данных.
Вы пересоздадите базу данных people.db
с помощью скрипта create_db.py
, который также содержит некоторые данные о персонажах и открытках. Вот фрагмент набора данных, с которым вы будете работать:
PEOPLE_NOTES = [
{
"lname": "Fairy",
"fname": "Tooth",
"notes": [
("I brush my teeth after each meal.", "2022-01-06 17:10:24"),
("The other day a friend said I have big teeth.", "2022-03-05 22:17:54"),
("Do you pay per gram?", "2022-03-05 22:18:10"),
],
},
# ...
]
Вы узнаете, как настроить базу данных SQLite для реализации связей персонажей и открыток. После этого вы сможете преобразовать словарь PEOPLE_NOTES
в данные, соответствующие структуре вашей базы данных.
Наконец, вы отобразите содержимое своей базы данных на домашней странице своего приложения и будете использовать API Flask REST для добавления, обновления и удаления открыток, которые вы отправляете персонажам.
Поехали!
В идеале вы последовательно выполнили первую и вторую части этого руководства, прежде чем приступить к третьей части, которую вы изучаете прямо сейчас.
Прежде чем продолжить, убедитесь, что структура вашего проекта выглядит следующим образом:
rp_flask_api/
├── templates/
│ └── home.html
├── venv/ # виртуальное окружение
├── app.py
├── config.py
├── create_db.py # файл создания и первоначального заполнения базы данных
├── models.py
├── people.db # файл базы данных
├── people.py
├── requirements.txt # файл с указанием зависимостей, установка через pip install -r requirements.txt
└── swagger.yml
Добавление зависимостей
Прежде чем продолжить работу над проектом Flask, хорошей идеей будет пересоздать и активировать виртуальную среду. Таким образом, вы установите все зависимости проекта не на всю систему, а только в виртуальную среду вашего проекта.
Если в папке вашего приложения присутствует папка venv
с виртуальным окружением, удалите ее, чтобы установить зависимости заново. Потом выберите свою операционную систему ниже и используйте команды создания и первоначальной настройки виртуальной среды:
Windows
python -m venv venv
.\venv\Scripts\activate
Linux + macOS
python -m venv venv
source venv/bin/activate
С помощью команд, показанных выше, вы создаете и активируете виртуальную среду с именем venv
, используя встроенный модуль Python venv
. Название виртуального окружения в скобочках (venv)
перед приглашением указывают на то, что вы успешно активировали виртуальную среду.
Если возникнут трудности с запуском приложения или с виртуальным окружением, повторите действия по установке зависимостей из второй части данного руководства.
Теперь вы можете убедиться, что ваше приложение Flask работает без ошибок: выполните следующую команду в каталоге, содержащем файл app.py
:
(venv) $ python app.py
При запуске этого приложения веб-сервер запустится на порту 8000. Если вы откроете браузер и перейдете по адресу http://localhost:8000
, вы должны увидеть страницу с заголовком Hello, People!
и списком персонажей из базы данных:
Подготовка набора данных
Прежде чем составлять структуру базы данных, хорошей идеей будет взглянуть на модель данных, которые в настоящее время содержит ваша база данных, и на набор данных, с которым вы будете работать.
Таблица person
вашей базы данных в файле people.db
в настоящее время выглядит следующим образом:
id | lname | fname | timestamp |
1 | Fairy | Tooth | 2022-10-08 09:15:10 |
2 | Ruprecht | Knecht | 2022-10-08 09:15:13 |
3 | Bunny | Easter | 2022-10-08 09:15:27 |
Вы будете расширять свою базу данных списком открыток PEOPLE_NOTES
:
PEOPLE_NOTES = [
{
"lname": "Fairy",
"fname": "Tooth",
"notes": [
("I brush my teeth after each meal.", "2022-01-06 17:10:24"),
("The other day a friend said, I have big teeth.", "2022-03-05 22:17:54"),
("Do you pay per gram?", "2022-03-05 22:18:10"),
],
},
{
"lname": "Ruprecht",
"fname": "Knecht",
"notes": [
("I swear, I'll do better this year.", "2022-01-01 09:15:03"),
("Really! Only good deeds from now on!", "2022-02-06 13:09:21"),
],
},
{
"lname": "Bunny",
"fname": "Easter",
"notes": [
("Please keep the current inflation rate in mind!", "2022-01-07 22:47:54"),
("No need to hide the eggs this time.", "2022-04-06 13:03:17"),
],
},
]
Обратите внимание, что значения lname
в PEOPLE_NOTES
соответствуют содержимому вашего столбца lname
в таблице person
вашей базы данных people.db
.
В приведенном выше наборе данных каждая запись персонажа включает ключ, называемый notes
, который связан со списком, содержащим кортежи данных. Каждый кортеж в списке открыток представляет собой данные одной открытки, содержащую текстовое содержимое и временную метку.
Каждый отдельный персонаж связан с несколькими открытками, и каждая отдельная открытка связана только с одним персонажем. Эта иерархия данных известна как отношение «один ко многим», где один родительский объект связан со многими дочерними объектами. Вы увидите, как это отношение «один ко многим» реализуется в базе данных при помощью SQLAlchemy далее в этой статье.
Создание связей открыток с персонажами
Вместо того, чтобы расширять таблицу person
и пытаться представить иерархические данные в одной таблице, вы разобьете данные на несколько таблиц и соедините их.
Для таблицы person
никаких изменений не будет. Чтобы представить новую информацию об открытке, вы создадите новую таблицу с именем note
. Таблица открыток будет выглядеть следующим образом:
id | person_id | content | timestamp |
1 | 1 | I brush my teeth after each meal. | 2022-01-06 17:10:24 |
2 | 1 | The other day a friend said, I have big teeth. | 2022-03-05 22:17:54 |
3 | 1 | Do you pay per gram? | 2022-03-05 22:18:10 |
4 | 2 | I swear, I’ll do better this year. | 2022-01-01 09:15:03 |
5 | 2 | Really! Only good deeds from now on! | 2022-02-06 13:09:21 |
6 | 3 | Please keep the current inflation rate in mind! | 2022-01-07 22:47:54 |
7 | 3 | No need to hide the eggs this time. | 2022-04-06 13:03:17 |
Обратите внимание, что, как и таблица person
, таблица note
имеет уникальный идентификатор id
, который является первичным ключом для таблицы note
. Столбец person_id
создает связь с таблицей person
.
В то время как id
является первичным ключом для таблицы, person_id
— это то, что известно, как внешний ключ. Внешний ключ дает каждой записи в таблице note
указатель на первичный ключ записи person
, с которым она связана. Используя это, SQLAlchemy может собрать все открытки, связанные с каждым персонажем, связав первичный ключ person.id
с внешним ключом note.person_id
.
Почему бы не ограничиться одной таблицей?
База данных, которую вы создали, хранила данные в таблице, а таблица — это двумерный массив строк и столбцов. Можно ли представить словарь People
выше в виде одной таблицы строк и столбцов? Это можно сделать следующим образом в таблице базы данных person
:
id | lname | fname | timestamp | content | note_timestamp |
1 | Fairy | Tooth | 2022-10-08 09:15:10 | I brush my teeth after each meal. | 2022-01-06 17:10:24 |
2 | Fairy | Tooth | 2022-10-08 09:15:10 | The other day a friend said, I have big teeth. | 2022-03-05 22:17:54 |
3 | Fairy | Tooth | 2022-10-08 09:15:10 | Do you pay per gram? | 2022-03-05 22:18:10 |
4 | Ruprecht | Knecht | 2022-10-08 09:15:13 | I swear, I’ll do better this year. | 2022-01-01 09:15:03 |
5 | Ruprecht | Knecht | 2022-10-08 09:15:13 | Really! Only good deeds from now on! | 2022-02-06 13:09:21 |
6 | Easter | Bunny | 2022-10-08 09:15:27 | Please keep the current inflation rate in mind! | 2022-01-07 22:47:54 |
7 | Easter | Bunny | 2022-10-08 09:15:27 | No need to hide the eggs this time. | 2022-04-06 13:03:17 |
Таблица выше на самом деле будет работать. Все данные представлены, и один персонаж связан с набором различных открыток.
Концептуально структура таблицы выше имеет преимущество в том, что она относительно проста для понимания. Вы даже можете привести довод, что данные могут быть сохранены в файле CSV вместо базы данных.
Хотя структура таблицы выше будет работать, у нее есть некоторые реальные недостатки. К ним относятся следующие:
- Проблемы с использованием из-за избыточных данных
- Нелогичные названия столбцов, отражающие свойства разных сущностей: персонажей и открыток
- Сложность реализации отношений «один ко многим»
Чтобы представить набор открыток, все данные для каждого персонажа повторяются для каждой уникальной открытки. Таким образом, данные о персонаже являются избыточными. Это не такая уж большая проблема для ваших данных о персонаже, так как столбцов не так много. Но представьте, если бы у персонажа было намного больше столбцов. При увеличении количества хранимой информации кратно увеличивался бы объем повторяющейся информации - это рано или поздно могло бы стать проблемой физического объема базы данных, если бы вы имели дело с миллионами строк данных.
Наличие таких избыточных данных также может привести к проблемам обслуживания с течением времени. Например, что, если Easter Bunny решил, что пора сменить свое имя? Для этого необходимо обновить каждую запись открыток, содержащих имя Easter Bunny, чтобы данные были согласованными. Такая работа с базой данных может привести к несогласованности данных, особенно, если работа выполняется человеком, вручную выполняющим SQL-запрос.
Кроме того, присваивание имен столбцам становится нелогичным. В таблице выше есть столбец временной метки, который используется для отслеживания времени создания и обновления персонажа в таблице. Вы также хотите иметь аналогичную функциональность для времени создания и обновления открытки, но поскольку временная метка уже используется, используется уточняющее имя note_timestamp
.
Что, если вы хотите добавить дополнительные отношения «один ко многим» в таблицу персонажей? Например, вы можете решить включить детей персонажа или номера телефонов. У каждого человека может быть несколько детей и несколько номеров телефонов. С помощью словаря Python People
выше вы можете сделать это относительно легко, добавив ключи children
и phone_numbers
с новыми списками, содержащими данные.
Однако, представление этих новых отношений «один ко многим» в таблице базы данных Person
выше становится значительно более сложным. Каждое новое отношение «один ко многим» кратно увеличивает количество строк, необходимых для представления для каждой отдельной записи в дочерних данных. Кроме того, проблем, связанных с избыточностью данных, становится больше и их сложнее решать.
Необходимость хранения все более крупных и сложных структур данных повышает популярность баз данных NoSQL. Эти системы баз данных позволяют разработчикам эффективно хранить гетерогенные данные, которые не структурированы в таблицах. Если вам интересно узнать о базах данных NoSQL, ознакомьтесь с Python и MongoDB: подключение к базам данных NoSQL.
Наконец, с данными, которые вы получите из приведенной выше структуры таблицы, будет неудобно работать, поскольку это будет просто большой список списков.
Разбив набор данных на две таблицы и введя концепцию внешнего ключа, вы немного усложните представление данных. Но вы устраните недостатки представления в виде одной таблицы.
Самым большим преимуществом связанных таблиц является тот факт, что в базе данных нет избыточных данных. Для каждого персонажа, которого вы хотите сохранить в базе данных, есть только одна запись. Если Easter Bunnny все еще хочет изменить имя, то вам придется изменить только одну строку в таблице персонажей, и все остальное, связанное с этой строкой, немедленно воспользуется преимуществами изменения.
Кроме того, наименование столбцов более единообразно и осмысленно. Поскольку данные персонажей и открыток находятся в отдельных таблицах, метка времени создания или обновления может быть единообразно названа в обеих таблицах, так как нет конфликта имен между таблицами.
Но, хватит теории! В следующем разделе вы создадите модели, которые представляют придуманные вами связи таблиц базы данных.
Расширение вашей базы данных
В этом разделе вы расширите свою базу данных. Вы собираетесь изменить структуру данных People
в models.py
, чтобы предоставить каждому персонажу список открыток, связанных с ними. Наконец, вы заполните базу данных некоторыми начальными данными.
Создание моделей данных в SQLAlchemy
Чтобы использовать две таблицы выше и использовать связь между ними, вам нужно будет создать модели SQLAlchemy, которые знают об обеих таблицах и связи между ними.
Начните с обновления модели Person
в models.py
, чтобы включить связь с коллекцией открыток:
# models.py
from datetime import datetime
from config import db, ma
class Person(db.Model):
__tablename__ = "person"
id = db.Column(db.Integer, primary_key=True)
lname = db.Column(db.String(32), unique=True)
fname = db.Column(db.String(32))
timestamp = db.Column(
db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow
)
notes = db.relationship(
Note,
backref="person",
cascade="all, delete, delete-orphan",
single_parent=True,
order_by="desc(Note.timestamp)"
)
# ...
В этом коде вы создаете новый атрибут в классе Person
с именем .notes
. Этот новый атрибут .notes
определяется в следующих строках кода:
- Вы создаете новый атрибут с именем .notes
и устанавливаете его равным экземпляру объекта с именем db.relationship
. Этот объект создает отношение, которое вы добавляете к классу Person
, и оно создается со всеми параметрами, определенными в следующих строках.
- Параметр Note
определяет класс SQLAlchemy, с которым будет связан класс Person
. Класс Note
еще не определен, поэтому на данный момент он не будет работать. Иногда может быть проще ссылаться на классы как на строки, чтобы избежать проблем с тем, какой класс определен первым. Например, здесь вы можете использовать "Note
" вместо Note
.
- Параметр backref="person"
создает то, что известно как обратная ссылка в объектах Note
. Каждый экземпляр Note
будет содержать атрибут с именем .person
. Атрибут .person
ссылается на родительский объект, с которым связан конкретный экземпляр Note
. Наличие ссылки на родительский объект (в данном случае Person
) в дочернем объекте может быть очень полезным, если ваш код выполняет итерации по открыткам и должен включать информацию о родительском элементе - персонаже.
- Параметр cascade="all, delete, delete-orphan"
определяет, как обрабатывать экземпляры Note
при внесении изменений в родительский экземпляр Person
. Например, когда удаляется объект Person
, SQLAlchemy создаст SQL-запрос, необходимый для удаления объекта Person
из базы данных. Этот параметр сообщает SQLAlchemy, что также необходимо удалить все экземпляры Note
, связанные с ним. Подробнее об этих параметрах можно прочитать в документации SQLAlchemy.
- Параметр single_parent=True
требуется, если delete-orphan
является частью предыдущего параметра cascade
. Это сообщает SQLAlchemy, что нельзя допускать существования потерянного, осиротевшего Note
, то есть Note
без родительского объекта Person
, поскольку у каждого Note
есть как минимум один родитель.
- Параметр order_by="desc(Note.timestamp)"
сообщает SQLAlchemy, как сортировать экземпляры Note
, связанные с объектом Person
. При извлечении объекта Person
по умолчанию список атрибутов Notes
будет содержать объекты Note
в неизвестном порядке. Функция SQLAlchemy desc()
отсортирует заметки в порядке убывания от самых новых к самым старым, а не в порядке возрастания по умолчанию.
Теперь, когда ваша модель Person
имеет новый атрибут .notes
, и это представляет отношение "один ко многим" к объектам Note
, вам нужно определить модель SQLAlchemy для объекта Note
. Поскольку вы ссылаетесь на Note
из Person
, добавьте новый класс Note
прямо перед определением класса Person
:
# models.py
from datetime import datetime
from config import db, ma
class Note(db.Model):
__tablename__ = "note"
id = db.Column(db.Integer, primary_key=True)
person_id = db.Column(db.Integer, db.ForeignKey("person.id"))
content = db.Column(db.String, nullable=False)
timestamp = db.Column(
db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow
)
class Person(db.Model):
# ...
# ...
Класс Note
определяет атрибуты открытки, как вы узнали из таблицы базы данных открыток выше. С помощью этого кода вы определяете атрибуты:
- Класс Note
, наследуемый от db.Model
, точно так же, как вы делали это ранее при создании класса Person
.
- __tablename__ = "note"
сообщает классу, какую таблицу базы данных использовать для хранения объектов Note
.
- Атрибут .id
, определяя его как целочисленное значение и как первичный ключ для объекта Note
.
- Атрибут .person_id
и определяет его как внешний ключ, связывая класс Note
с классом Person
с помощью первичного ключа .person.id
. Это и атрибут Person.notes
— то, как SQLAlchemy знает, что делать при взаимодействии с объектами Person
и Note
.
- Атрибут .content
, который содержит фактический текст открытки. Параметр nullable=False
указывает, что содержимое новых открыток не должно быть пустым.
- Атрибут .timestamp
, и точно так же, как в классе Person
, этот атрибут содержит время создания или обновления для любого конкретного экземпляра Note
.
Теперь, когда вы обновили People
и создали модель для Note
, переходите к обновлению базы данных.
Обновление базы данных
Теперь, когда вы обновили Person
и создали модель Note
, вы будете использовать их для перестроения базы данных people.db
. Для этого переделайте вспомогательный скрипт Python с именем create_db.py
:
# create_db.py
from datetime import datetime
from config import app, db
from models import Person, Note
PEOPLE_NOTES = [
{
"lname": "Fairy",
"fname": "Tooth",
"notes": [
("I brush my teeth after each meal.", "2022-01-06 17:10:24"),
("The other day a friend said, I have big teeth.", "2022-03-05 22:17:54"),
("Do you pay per gram?", "2022-03-05 22:18:10"),
],
},
{
"lname": "Ruprecht",
"fname": "Knecht",
"notes": [
("I swear, I'll do better this year.", "2022-01-01 09:15:03"),
("Really! Only good deeds from now on!", "2022-02-06 13:09:21"),
],
},
{
"lname": "Bunny",
"fname": "Easter",
"notes": [
("Please keep the current inflation rate in mind!", "2022-01-07 22:47:54"),
("No need to hide the eggs this time.", "2022-04-06 13:03:17"),
],
},
]
with app.app_context():
db.drop_all()
db.create_all()
for data in PEOPLE_NOTES:
new_person = Person(lname=data.get("lname"), fname=data.get("fname"))
for content, timestamp in data.get("notes", []):
new_person.notes.append(
Note(
content=content,
timestamp=datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S"),
)
)
db.session.add(new_person)
db.session.commit()
В коде выше вы загружаете в базу данных вашего проекта содержимое PEOPLE_NOTES
. Вы используете db
из вашего config
, чтобы Python знал, как обрабатывать данные и фиксировать их в соответствующих таблицах и ячейках базы данных.
При выполнении
create_db.py
вы заново создадитеpeople.db
. Все существующие данные в people.db будут потеряны.
Запуск программы create_db.py
из командной строки заново создаст базу данных с новыми дополнениями, подготовив ее к использованию в приложении:
(venv) $ python create_db.py
Как только ваш проект получит обновленную базу данных, вы можете настроить его для отображения открыток в веб-интерфейсе.
Отображение персонажей с их открытками
Теперь, когда ваша база данных содержит данные для работы, вы можете начать отображать данные как в интерфейсе пользователя, так и в вашем REST API.
Отображение заметок в веб-интерфейсе
В предыдущем разделе вы создали связь между персонажем и его открытками, добавив атрибут .notes
в класс Person
. Обновите home.html
в папке templates/
, чтобы отобразить открытки персонажей:
<!-- templates/home.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>RP Flask REST API</title>
</head>
<body>
<h1>Hello, People!</h1>
{% for person in people %}
<h2>{{ person.fname }} {{ person.lname }}</h2>
<ul>
{% for note in person.notes %}
<li>{{ note.content }}</li>
{% endfor %}
</ul>
{% endfor %}
</body>
</html>
В коде выше вы получаете доступ к атрибуту .notes
каждого персонажа. После этого вы проходите по всем открыткам для конкретного персонажа, чтобы вывести содержимое открытки.
Перейдите по адресу http://localhost:8000
, чтобы проверить, отображается ли ваш шаблон так, как ожидалось:
Составление запросов к API открыток
Далее проверьте конечную точку /api/people
вашего API по адресу http://localhost:8000/api/people
:
Чтобы выявить проблему, посмотрите на read_all()
в people.py
:
# people.py
# ...
def read_all():
people = Person.query.all()
person_schema = PersonSchema(many=True)
return person_schema.dump(people)
# ...
Метод .dump()
работает с тем, что получает, и не отфильтровывает никакие данные. Так что проблема может быть в определении people
или person_schema
.
Вызов запроса к базе данных для заполнения people точно такой же, как в app.py:
Person.query.all()
Этот вызов успешно отработал на фронтенде, чтобы показать открытки для каждого человека. Это выделяет PersonSchema
как наиболее вероятного виновника.
По умолчанию схема Marshmallow не переходит в связанные объекты базы данных. Вам нужно явно указать схеме включить обработку отношений.
Откройте models.py
и обновите PersonSchema
:
# models.py
# ...
class PersonSchema(ma.SQLAlchemyAutoSchema):
class Meta:
model = Person
load_instance = True
sqla_session = db.session
include_relationships = True
С параметром include_relationships
в классе Meta
PersonSchema
вы сообщаете Marshmallow о необходимости добавить любые связанные объекты в схему данных person
. Однако результат все еще не выглядит списком открыток, как ожидалось:
Создание схемы данных для открытки
Ваш API в ответе перечислил только первичные ключи открыток каждого персонажа. Это справедливо, потому что вы еще не объявили, как Marshmallow должен десериализовать открытки.
Помогите Marshmallow, создав NoteSchema
в models.py
ниже Note
и выше Person
:
# models.py
# ...
class Note(db.Model):
# ...
class NoteSchema(ma.SQLAlchemyAutoSchema):
class Meta:
model = Note
load_instance = True
sqla_session = db.session
include_fk = True
class Person(db.Model):
# ...
class PersonSchema(ma.SQLAlchemyAutoSchema):
# ...
note_schema = NoteSchema()
# ...
Вы ссылаетесь на Note
из NoteSchema
, поэтому вы должны поместить NoteSchema
ниже определения вашего класса Note
, чтобы избежать ошибок. Вы также создаете экземпляр NoteSchema
, чтобы создать объект, на который вы будете ссылаться позже.
Поскольку ваша модель данных Note
содержит внешний ключ, вы должны установить include_fk
в True
. В противном случае Marshmallow не распознает person_id
во время процесса сериализации.
При наличии NoteSchema
вы можете ссылаться на него в PeopleSchema
:
# models.py
from datetime import datetime
from marshmallow_sqlalchemy import fields
from config import db, ma
# ...
class PersonSchema(ma.SQLAlchemyAutoSchema):
class Meta:
model = Person
load_instance = True
sqla_session = db.session
include_relationships = True
notes = fields.Nested(NoteSchema, many=True)
После импорта fields
из marshmallow_sqlalchemy вы можете ссылаться на связанный объект Note
по его NoteSchema
. Чтобы избежать ошибок, убедитесь, что вы определили NoteSchema
выше PeopleSchema
.
Хотя вы работаете с SQLAlchemyAutoSchema, вам необходимо явно создать поле notes
в PersonSchema
. В противном случае Marshmallow не получит всю необходимую информацию для работы с данными Notes
. Например, он не будет знать, что вы ожидаете список объектов, используя аргумент many
.
С внесенными изменениями проверьте конечную точку вашего API по адресу http://localhost:8000/api/people
:
В следующем разделе вы расширите свой API Flask REST для создания, чтения, обновления и удаления открыток.
Обработка открыток с помощью вашего API REST
Вы обновили модели данных SQLAlchemy и использовали их для чтения из базы данных people.db
. Ваши открытки доступны в виде вложенной схемы в People
. Вы получаете список открыток, когда запрашиваете коллекцию персонажей или конкретного персонажа:
Действие | HTTP-метод | URL | Описание |
Чтение | GET | /api/people | Получение списка персонажей |
Чтение | GET | /api/people/<lname> | Получение данных определенного персонажа |
Хотя вы можете читать заметки через конечные точки, показанные в таблице выше, в настоящее время нет возможности читать только одну заметку или управлять любыми заметками в вашем REST API.
Параметры URL чувствительны к регистру. Например, вы должны посетить http://localhost:8000/api/people/Ruprecht
с заглавной буквой R
в фамилии Ruprecht
.
Вы можете перейти к первой части, чтобы подвести итог тому, как вы создали существующие конечные точки people
вашего REST API. В этом разделе руководства вы добавите дополнительные конечные точки, чтобы обеспечить функциональность для создания, чтения, обновления и удаления открыток:
Действие | HTTP-метод | URL | Описание |
Создание | POST | /api/notes | Создает новую открытку |
Чтение | GET | /api/notes/<note_id> | Получение данных конкретной открытки |
Обновление | PUT | api/notes/<note_id> | Обновление данных конкретной открытки |
Удаление | DELETE | api/notes/<note_id> | Удаление конкретной открытки |
Вы начнете с добавления функциональности для чтения одной открытки. Для этого вам нужно будет настроить файл конфигурации Swagger, содержащий аннотации API.
Чтение одной открытки
В настоящее время вы можете получать все открытки персонажа, когда запрашиваете данные этого персонажа. Чтобы получить информацию об одной открытке, вам нужно будет добавить еще одну конечную точку.
Перед добавлением конечной точки обновите конфигурацию Swagger, создав компонент параметра note_id
в файле swagger.yml
:
# swagger.yml
# ...
components:
schemas:
# ...
parameters:
lname:
# ...
note_id:
name: "note_id"
description: "ID of the note"
in: path
required: true
schema:
type: "integer"
# ...
Параметр note_id
будет частью ваших конечных точек для определения того, какую открытку вы хотите обработать.
Продолжайте редактировать swagger.yml
и добавьте данные для конечной точки для чтения одной заметки:
# swagger.yml
# ...
paths:
/people:
# ...
/people/{lname}:
# ...
/notes/{note_id}:
get:
operationId: "notes.read_one"
tags:
- Notes
summary: "Read one note"
parameters:
- $ref: "#/components/parameters/note_id"
responses:
"200":
description: "Successfully read one note"
Структура /notes/{note_id}
похожа на /people/{lname}
. Вы начинаете с операции get
для пути /notes/{note_id}
. Подстрока {note_id}
является значением для идентификатора заметки, которое вы должны передать как параметр URL. Так, например, URL http://localhost:8000/api/notes/1
предоставит вам данные для заметки с первичным ключом 1.
OperationId
указывает на notes.read_one
. Это означает, что ваш API ожидает функцию read_one()
в файле notes.py
. Продолжайте, создайте файл notes.py
и добавьте туда функцию read_one()
:
# notes.py
from flask import abort, make_response
from config import db
from models import Note, note_schema
def read_one(note_id):
note = Note.query.get(note_id)
if note is not None:
return note_schema.dump(note)
else:
abort(
404, f"Note with ID {note_id} not found"
)
Хотя вы пока не используете make_response()
и db
, вы можете продолжить и добавить их в свои импорты. Вы будете использовать их немного позже, когда будете записывать в базу данных.
На данный момент вы только читаете из базы данных с параметром note_id
из пути URL REST. Вы используете note_id
в методе .get()
запроса, чтобы получить заметку с первичным ключом целого числа note_id
.
Если открытка найдена, то note содержит объект Note
, и вы возвращаете сериализованный объект. Попробуйте, посетив http://localhost:8000/api/notes/1
в своем браузере:
Обновление и удаление открытки
На этот раз вы начнете с создания функций в notes.py
, прежде чем создавать операции в swagger.yml
.
Добавьте update()
и delete()
в notes.py
:
# notes.py
# ...
def update(note_id, note):
existing_note = Note.query.get(note_id)
if existing_note:
update_note = note_schema.load(note, session=db.session)
existing_note.content = update_note.content
db.session.merge(existing_note)
db.session.commit()
return note_schema.dump(existing_note), 201
else:
abort(404, f"Note with ID {note_id} not found")
def delete(note_id):
existing_note = Note.query.get(note_id)
if existing_note:
db.session.delete(existing_note)
db.session.commit()
return make_response(f"{note_id} successfully deleted", 204)
else:
abort(404, f"Note with ID {note_id} not found")
Если сравнить update()
и delete()
, то они имеют схожую структуру. Обе функции ищут существующую открытку и работают с сессией базы данных.
Для работы update()
вы также принимаете объект note в качестве аргумента, который содержит атрибут .content
, который вы можете обновить.
Напротив, вам нужно знать только ID заметки, от которой вы хотите избавиться при вызове delete()
.
Затем создайте две операции в swagger.yml
, которые ссылаются на notes.update
и notes.delete
:
# swagger.yml
# ...
paths:
/people:
# ...
/people/{lname}:
# ...
/notes/{note_id}:
get:
# ...
put:
tags:
- Notes
operationId: "notes.update"
summary: "Update a note"
parameters:
- $ref: "#/components/parameters/note_id"
responses:
"200":
description: "Successfully updated note"
requestBody:
x-body-name: "note"
content:
application/json:
schema:
type: "object"
properties:
content:
type: "string"
delete:
tags:
- Notes
operationId: "notes.delete"
summary: "Delete a note"
parameters:
- $ref: "#/components/parameters/note_id"
responses:
"204":
description: "Successfully deleted note"
Опять же, структуры put
и delete
похожи. Главное отличие в том, что вам нужно предоставить requestBody
, который содержит данные открытки для обновления объекта базы данных.
Теперь вы создали конечные точки для работы с существующими открытками. Далее вы добавите конечную точку для создания открытки.
Создание открытки для персонажа
До сих пор вы могли читать, обновлять и удалять одну открытку. Это действия, которые вы можете выполнять с существующими открытками. Теперь пришло время добавить функциональность в ваш REST API, чтобы также создавать новую открытку. Добавьте create()
в notes.py
:
# notes.py
from flask import make_response, abort
from config import db
from models import Note, Person, note_schema
# ...
def create(note):
person_id = note.get("person_id")
person = Person.query.get(person_id)
if person:
new_note = note_schema.load(note, session=db.session)
person.notes.append(new_note)
db.session.commit()
return note_schema.dump(new_note), 201
else:
abort(
404, f"Person not found for ID: {person_id}"
)
Открытка всегда должна принадлежать персонажу. Вот почему вам нужно работать с моделью данных Person
при создании новой открытки.
Сначала вы ищете владельца открытки, используя person_id
, который вы предоставляете с аргументом notes
для create()
. Если этот персонаж существует в базе данных, то вы переходите к добавлению новой открытки в person.notes
.
Хотя в этом случае вы работаете с таблицей базы данных person
, SQLAlchemy позаботится о том, чтобы открытка была добавлена в таблицу открыток.
Чтобы получить доступ к notes.create
с помощью вашего API, перейдите в swagger.yml
и добавьте еще одну конечную точку:
# swagger.yml
# ...
paths:
/people:
# ...
/people/{lname}:
# ...
/notes:
post:
operationId: "notes.create"
tags:
- Notes
summary: "Create a note associated with a person"
requestBody:
description: "Note to create"
required: True
x-body-name: "note"
content:
application/json:
schema:
type: "object"
properties:
person_id:
type: "integer"
content:
type: "string"
responses:
"201":
description: "Successfully created a note"
/notes/{note_id}:
# ...
Вы добавляете конечную точку /notes
прямо перед конечной точкой /notes/{noted_id}
. Таким образом, вы упорядочиваете конечные точки открыток от общих к частным. Этот порядок помогает вам ориентироваться в файле swagger.yml
, когда ваш API разрастается.
С данными в блоке схемы вы предоставляете Marshmallow информацию о том, как сериализовать открытку в вашем API. Если вы сравните эту схему данных Note
с моделью Note
в models.py
, то заметите, что имена person_id
и content
совпадают. То же самое касается типов полей.
Вы также можете заметить, что не все поля модели открытки присутствуют в схеме компонента. Это нормально, потому что вы будете использовать эту схему только для публикации новых открыток. Для каждой открытки идентификатор и временная метка будут установлены автоматически.
После того, как все конечные точки для обработки ваших открыток добавлены, пора взглянуть на аннотации API.
Аннотации API
С внесением вышеуказанных изменений вы можете использовать свой API для добавления, обновления и удаления открыток. Посетите свой веб-интерфейс Swagger по адресу http://localhost:8000/api/ui
и убедитесь, что ваши конечные точки API Flask REST работают! Любые изменения, которые вы вносите с помощью своего API, также отображаются на вашем веб-интерфейсе.
Заключение
В этой статье вы настроили свою базу данных SQLite для реализации связей. После этого вы перевели словарь PEOPLE_NOTE
S в данные, соответствующие структуре вашей базы данных, и превратили свой API Flask REST в веб-приложение для хранения заметок.
В этой третьей части серии вы узнали, как:
- Работать с несколькими таблицами с взаимосвязанной информацией в базе данных
- Создавать связи «один ко многим» в базе данных
- Управлять связями с помощью SQLAlchemy
- Сериализовать сложные схемы данных со связями с помощью Marshmallow
- Отображать связанные объекты в клиентском интерфейсе
Понимание того, как создавать и использовать связи данных в базе данных, дает вам мощный инструмент для решения многих сложных проблем. Помимо примера «один ко многим» из этого руководства существуют и другие связи. Другими распространенными являются «один к одному», «многие ко многим» и «многие к одному». Все они занимают свое место в вашем арсенале, и SQLAlchemy поможет вам справиться с ними всеми!
Вы успешно создали API REST для отслеживания открыток персонажей, которые могут посетить вас в течение года. В вашей базе данных есть такие персонажи, как Зубная фея, Пасхальный кролик и Кнехт Рупрехт. Добавляя открытки, вы можете отслеживать свои добрые дела и, возможно, получать от них ценные подарки.
Полный исходный код
Структура файлов и папок
rp_flask_api/
├── templates/
│ └── home.html
├── venv/ # виртуальное окружение
├── app.py
├── config.py
├── create_db.py # файл создания и первоначального заполнения базы данных
├── models.py
├── notes.py
├── people.db # файл базы данных
├── people.py
├── requirements.txt # файл с указанием зависимостей, установка через pip install -r requirements.txt
└── swagger.yml
templates/home.html
<!-- templates/home.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>RP Flask REST API</title>
</head>
<body>
<h1>Hello, People!</h1>
{% for person in people %}
<h2>{{ person.fname }} {{ person.lname }}</h2>
<ul>
{% for note in person.notes %}
<li>{{ note.content }}</li>
{% endfor %}
</ul>
{% endfor %}
</body>
</html>
app.py
# app.py
import config
from flask import render_template
from models import Person
app = config.connex_app
app.add_api(config.basedir / "swagger.yml")
@app.route("/")
def home():
people = Person.query.all()
return render_template("home.html", people=people)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
config.py
# config.py
import pathlib
import connexion
from flask_marshmallow import Marshmallow
from flask_sqlalchemy import SQLAlchemy
basedir = pathlib.Path(__file__).parent.resolve()
connex_app = connexion.App(__name__, specification_dir=basedir)
app = connex_app.app
app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{basedir / 'people.db'}"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
db = SQLAlchemy(app)
ma = Marshmallow(app)
create_db.py
# create_db.py
from datetime import datetime
from config import app, db
from models import Person, Note
PEOPLE_NOTES = [
{
"lname": "Fairy",
"fname": "Tooth",
"notes": [
("I brush my teeth after each meal.", "2022-01-06 17:10:24"),
("The other day a friend said, I have big teeth.", "2022-03-05 22:17:54"),
("Do you pay per gram?", "2022-03-05 22:18:10"),
],
},
{
"lname": "Ruprecht",
"fname": "Knecht",
"notes": [
("I swear, I'll do better this year.", "2022-01-01 09:15:03"),
("Really! Only good deeds from now on!", "2022-02-06 13:09:21"),
],
},
{
"lname": "Bunny",
"fname": "Easter",
"notes": [
("Please keep the current inflation rate in mind!", "2022-01-07 22:47:54"),
("No need to hide the eggs this time.", "2022-04-06 13:03:17"),
],
},
]
with app.app_context():
db.drop_all()
db.create_all()
for data in PEOPLE_NOTES:
new_person = Person(lname=data.get("lname"), fname=data.get("fname"))
for content, timestamp in data.get("notes", []):
new_person.notes.append(
Note(
content=content,
timestamp=datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S"),
)
)
db.session.add(new_person)
db.session.commit()
models.py
# models.py
from datetime import datetime
from marshmallow_sqlalchemy import fields
from config import db, ma
class Note(db.Model):
__tablename__ = "note"
id = db.Column(db.Integer, primary_key=True)
person_id = db.Column(db.Integer, db.ForeignKey("person.id"))
content = db.Column(db.String, nullable=False)
timestamp = db.Column(
db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow
)
class NoteSchema(ma.SQLAlchemyAutoSchema):
class Meta:
model = Note
load_instance = True
sqla_session = db.session
include_fk = True
class Person(db.Model):
__tablename__ = "person"
id = db.Column(db.Integer, primary_key=True)
lname = db.Column(db.String(32), unique=True)
fname = db.Column(db.String(32))
timestamp = db.Column(
db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow
)
notes = db.relationship(
Note,
backref="person",
cascade="all, delete, delete-orphan",
single_parent=True,
order_by="desc(Note.timestamp)"
)
class PersonSchema(ma.SQLAlchemyAutoSchema):
class Meta:
model = Person
load_instance = True
sqla_session = db.session
include_relationships = True
notes = fields.Nested(NoteSchema, many=True)
note_schema = NoteSchema()
person_schema = PersonSchema()
people_schema = PersonSchema(many=True)
nodes.py
# notes.py
from flask import abort, make_response
from config import db
from models import Note, Person, note_schema
def read_one(note_id):
note = Note.query.get(note_id)
if note is not None:
return note_schema.dump(note)
else:
abort(
404, f"Note with ID {note_id} not found"
)
def create(note):
person_id = note.get("person_id")
person = Person.query.get(person_id)
if person:
new_note = note_schema.load(note, session=db.session)
person.notes.append(new_note)
db.session.commit()
return note_schema.dump(new_note), 201
else:
abort(
404, f"Person not found for ID: {person_id}"
)
def update(note_id, note):
existing_note = Note.query.get(note_id)
if existing_note:
update_note = note_schema.load(note, session=db.session)
existing_note.content = update_note.content
db.session.merge(existing_note)
db.session.commit()
return note_schema.dump(existing_note), 201
else:
abort(404, f"Note with ID {note_id} not found")
def delete(note_id):
existing_note = Note.query.get(note_id)
if existing_note:
db.session.delete(existing_note)
db.session.commit()
return make_response(f"{note_id} successfully deleted", 204)
else:
abort(404, f"Note with ID {note_id} not found")
people.py
# people.py
from config import db
from flask import abort, make_response
from models import Person, people_schema, person_schema
def read_all():
people = Person.query.all()
return people_schema.dump(people)
def create(person):
lname = person.get("lname")
existing_person = Person.query.filter(Person.lname == lname).one_or_none()
if existing_person is None:
new_person = person_schema.load(person, session=db.session)
db.session.add(new_person)
db.session.commit()
return person_schema.dump(new_person), 201
else:
abort(406, f"Person with last name {lname} already exists")
def read_one(lname):
person = Person.query.filter(Person.lname == lname).one_or_none()
if person is not None:
return person_schema.dump(person)
else:
abort(404, f"Person with last name {lname} not found")
def update(lname, person):
existing_person = Person.query.filter(Person.lname == lname).one_or_none()
if existing_person:
update_person = person_schema.load(person, session=db.session)
existing_person.fname = update_person.fname
db.session.merge(existing_person)
db.session.commit()
return person_schema.dump(existing_person), 201
else:
abort(404, f"Person with last name {lname} not found")
def delete(lname):
existing_person = Person.query.filter(Person.lname == lname).one_or_none()
if existing_person:
db.session.delete(existing_person)
db.session.commit()
return make_response(f"{lname} successfully deleted", 200)
else:
abort(404, f"Person with last name {lname} not found")
requirements.txt
# requirements.txt
connexion[Flask]==3.1.0
connexion[uvicorn]==3.1.0
flask-marshmallow==1.2.1
Flask-SQLAlchemy==3.1.1
marshmallow-sqlalchemy==1.1.0
swagger_ui_bundle==1.1.0
swagger.yml
# swagger.yml
openapi: 3.0.0
info:
title: "RP Flask REST API"
description: "An API about people and notes"
version: "1.0.0"
servers:
- url: "/api"
components:
schemas:
Person:
type: "object"
required:
- lname
properties:
fname:
type: "string"
lname:
type: "string"
parameters:
lname:
name: "lname"
description: "Last name of the person to get"
in: path
required: True
schema:
type: "string"
note_id:
name: "note_id"
description: "ID of the note"
in: path
required: true
schema:
type: "integer"
paths:
/people:
get:
operationId: "people.read_all"
tags:
- "People"
summary: "Read the list of people"
responses:
"200":
description: "Successfully read people list"
post:
operationId: "people.create"
tags:
- People
summary: "Create a person"
requestBody:
x-body-name: "person"
description: "Person to create"
required: True
content:
application/json:
schema:
$ref: "#/components/schemas/Person"
responses:
"201":
description: "Successfully created person"
/people/{lname}:
get:
operationId: "people.read_one"
tags:
- People
summary: "Read one person"
parameters:
- $ref: "#/components/parameters/lname"
responses:
"200":
description: "Successfully read person"
put:
tags:
- People
operationId: "people.update"
summary: "Update a person"
parameters:
- $ref: "#/components/parameters/lname"
responses:
"200":
description: "Successfully updated person"
requestBody:
x-body-name: "person"
content:
application/json:
schema:
$ref: "#/components/schemas/Person"
delete:
tags:
- People
operationId: "people.delete"
summary: "Delete a person"
parameters:
- $ref: "#/components/parameters/lname"
responses:
"204":
description: "Successfully deleted person"
/notes:
post:
operationId: "notes.create"
tags:
- Notes
summary: "Create a note associated with a person"
requestBody:
description: "Note to create"
required: True
x-body-name: "note"
content:
application/json:
schema:
type: "object"
properties:
person_id:
type: "integer"
content:
type: "string"
responses:
"201":
description: "Successfully created a note"
/notes/{note_id}:
get:
operationId: "notes.read_one"
tags:
- Notes
summary: "Read one note"
parameters:
- $ref: "#/components/parameters/note_id"
responses:
"200":
description: "Successfully read one note"
put:
tags:
- Notes
operationId: "notes.update"
summary: "Update a note"
parameters:
- $ref: "#/components/parameters/note_id"
responses:
"200":
description: "Successfully updated note"
requestBody:
x-body-name: "note"
content:
application/json:
schema:
type: "object"
properties:
content:
type: "string"
delete:
tags:
- Notes
operationId: "notes.delete"
summary: "Delete a note"
parameters:
- $ref: "#/components/parameters/note_id"
responses:
"204":
description: "Successfully deleted note"