Во 2 части этой серии вы добавили возможность сохранять изменения, внесенные через REST API, в базу данных с помощью SQLAlchemy и узнали, как сериализовать эти данные для REST API с помощью Marshmallow. Подключение REST API к базе данных таким образом, чтобы приложение могло вносить изменения в существующие данные и создавать новые данные, является отличным и делает приложение гораздо более полезным и надежным.
Однако это лишь часть той мощи, которую предлагает база данных. Еще более мощной особенностью является R-часть систем СУБД: отношения. В базе данных связь - это способность связывать две или более таблиц вместе осмысленным образом. В этой статье вы узнаете, как реализовать отношения и превратить базу данных Person в веб-приложение для мини-блогов.
В этой статье вы узнаете:
- Почему несколько таблиц в базе данных являются полезными и важными
- Как таблицы связаны друг с другом
- Как SQLAlchemy может помочь вам управлять отношениями
- Как отношения помогают вам создать мини-приложение для ведения блога
Для Кого Предназначена Эта Статья
1 часть этой серии помогла вам построить REST API, а 2 часть показала, как подключить этот REST API к базе данных.
Эта статья еще больше расширяет ваш пояс инструментов программирования. Вы узнаете, как создавать иерархические структуры данных, представленные в виде отношений "один ко многим" с помощью SQLAlchemy. Кроме того, вы расширите уже созданный REST API, чтобы обеспечить поддержку CRUD (создание, чтение, обновление и удаление) для элементов этой иерархической структуры.
Веб-приложение, представленное во 2 части, будет иметь свои HTML-и JavaScript-файлы, модифицированные в основном для создания более полнофункционального мини-приложения для ведения блогов. Вы можете просмотреть окончательную версию кода из 2 части в репозитории GitHub для этой статьи.
Держитесь, скоро вы начнете создавать отношения и свое мини-приложение для ведения блога!
Дополнительная Зависимость
Там нет никаких новых зависимостей Python, кроме того, что было необходимо для статьи части 2. Тем не менее, вы будете использовать два новых модуля JavaScript в веб-приложении, чтобы сделать вещи проще и более последовательными. Эти два модуля являются следующими:
- Handlebars.js-это шаблонный движок для JavaScript, очень похожий на Jinja2 для Flask.
- Moment.js - это модуль синтаксического анализа и форматирования datetime, который упрощает отображение временных меток UTC.
Вам не нужно загружать ни один из них, так как веб-приложение получит их непосредственно из Cloudflare CDN (сеть доставки контента), как вы уже делаете для модуля jQuery.
Расширение данных people для блога
Во 2 части существуют данные люди в качестве словаря в build_database.py Python-кода. Это то, что вы использовали для заполнения базы данных некоторыми исходными данными. Вы собираетесь изменить структуру данных людей, чтобы дать каждому человеку список заметок, связанных с ними. Новая структура данных о людях будет выглядеть следующим образом:
# Данные для инициализации БД PEOPLE = [ { "fname": "Doug", "lname": "Farrell", "notes": [ ("Cool, a mini-blogging application!", "2019-01-06 22:17:54"), ("This could be useful", "2019-01-08 22:17:54"), ("Well, sort of useful", "2019-03-06 22:17:54"), ], }, { "fname": "Kent", "lname": "Brockman", "notes": [ ( "I'm going to make really profound observations", "2019-01-07 22:17:54", ), ( "Maybe they'll be more obvious than I thought", "2019-02-06 22:17:54", ), ], }, { "fname": "Bunny", "lname": "Easter", "notes": [ ("Has anyone seen my Easter eggs?", "2019-01-07 22:47:54"), ("I'm really late delivering these!", "2019-04-06 22:17:54"), ], }, ]
Каждый человек в словаре людей теперь включает ключ под названием Заметки, который связан со списком, содержащим кортежи данных. Каждый Кортеж в списке заметок представляет собой одну заметку, содержащую содержимое и метку времени. Временные метки инициализируются (а не динамически создаются), чтобы продемонстрировать порядок позже в REST API.
Каждый отдельный человек связан с несколькими нотами, и каждая отдельная нота связана только с одним человеком. Эта иерархия данных называется отношением "один ко многим", когда один родительский объект связан со многими дочерними объектами. Вы увидите, как это отношение "один ко многим" управляется в базе данных с помощью SQLAlchemy.
Метод брут-форс
Построенная база данных хранит данные в виде таблицы, а таблица представляет собой двумерный массив строк и столбцов. Может ли приведенный выше словарь людей быть представлен в одной таблице строк и столбцов? Это может быть сделано следующим образом в вашей таблице базы данных person. К сожалению, чтобы включить все фактические данные в пример, создается полоса прокрутки для таблицы, как вы увидите ниже:
person_id | lname | fname | отметка времени | содержание | note_timestamp |
1 | farrell | doug | 2018-08-08 21:16:01 | cool, a mini-blogging application! | 2019-01-06 22:17:54 |
2 | farrell | doug | 2018-08-08 21:16:01 | this could be useful | 2019-01-06 22:17:54 |
3 | farrell | doug | 2018-08-08 21:16:01 | well, sort of useful | 2019-01-06 22:17:54 |
4 | brockman | kent | 2018-08-08 21:16:01 | i’m going to make really profound observations | 2019-01-06 22:17:54 |
5 | brockman | kent | 2018-08-08 21:16:01 | maybe they’ll be more obvious than i thought | 2019-01-06 22:17:54 |
6 | easter | bunny | 2018-08-08 21:16:01 | has anyone seen my easter eggs? | 2019-01-06 22:17:54 |
7 | easter | bunny | 2018-08-08 21:16:01 | i’m really late delivering these! | 2019-01-06 22:17:54 |
Приведенная выше таблица действительно будет работать. Все данные представлены, и один человек связан с коллекцией различных заметок.
Преимущества
С концептуальной точки зрения приведенная выше структура таблицы имеет то преимущество, что она относительно проста для понимания. Вы даже можете сделать так, что данные могут быть сохранены в плоском файле, а не в базе данных.
Из-за двумерной структуры таблицы вы можете хранить и использовать эти данные в электронной таблице. Электронные таблицы были введены в эксплуатацию в качестве хранилища данных совсем немного.
Недостатки
Хотя приведенная выше структура таблицы будет работать, она имеет некоторые реальные недостатки.
Для того чтобы представить коллекцию нот, все данные для каждого человека повторяются для каждой уникальной ноты, поэтому данные о человеке являются избыточными. Это не такая уж большая проблема для ваших персональных данных, поскольку их не так уж много. Но представьте себе, что у человека было бы гораздо больше колонок. Даже с большими дисковыми накопителями это может стать проблемой хранения, Если вы имеете дело с миллионами строк данных.
Наличие избыточных данных, подобных этому, со временем может привести к проблемам с обслуживанием. Например, Что делать, если Пасхальный кролик решил, что смена имени-это хорошая идея. Чтобы сделать это, каждая запись, содержащая имя Пасхального Кролика, должна быть обновлена, чтобы сохранить согласованность данных. Такая работа с базой данных может привести к несогласованности данных, особенно если она выполняется человеком, выполняющим SQL-запрос вручную.
Именование столбцов становится неудобным. В приведенной выше таблице есть столбец timestamp, используемый для отслеживания времени создания и обновления человека в таблице. Вы также хотите иметь аналогичную функциональность для времени создания и обновления заметки, но поскольку метка времени уже используется, Используется вымышленное имя note_timestamp.
Что делать,если вы хотите добавить дополнительные отношения "один ко многим" в таблицу person? Например, чтобы включить детей человека или номера телефонов. У каждого человека может быть несколько детей и несколько телефонных номеров. Это можно было бы сделать относительно легко для приведенного выше словаря Python People, добавив дочерние ключи и ключи phone_numbers с новыми списками, содержащими данные.
Однако представление этих новых отношений "один ко многим" в приведенной выше таблице базы данных person становится значительно сложнее. Каждое новое отношение "один ко многим" резко увеличивает число строк, необходимых для его представления для каждой отдельной записи в дочерних данных. Кроме того, проблемы, связанные с избыточностью данных, становятся все более масштабными и трудноразрешимыми.
Наконец, данные, которые вы получите из приведенной выше структуры таблицы, не будут очень Пифоническими: это будет просто большой список списков. SQLAlchemy не сможет вам очень сильно помочь, потому что там нет никаких отношений.
Реляционный Подход К Базам Данных
Исходя из того, что вы видели выше, становится ясно, что попытка представить даже умеренно сложный набор данных в одной таблице становится неуправляемой довольно быстро. Учитывая это, какую альтернативу может предложить база данных? Именно здесь в игру вступает R-часть баз данных СУБД. Представление отношений устраняет описанные выше недостатки.
Вместо того чтобы пытаться представить иерархические данные в одной таблице, эти данные разбиваются на несколько таблиц с механизмом их связи друг с другом. Таблицы разбиты по линиям сбора, поэтому для вашего словаря людей выше это означает, что будет таблица, представляющая людей, и другая, представляющая заметки. Это возвращает вашу оригинальную таблицу персон, которая выглядит следующим образом:
person_id | lname | fname | отметка времени |
1 | farrell | doug | 2018-08-08 21:16:01.888444 |
2 | brockman | kent | 2018-08-08 21:16:01.889060 |
3 | easter | bunny | 2018-08-08 21:16:01.886834 |
Чтобы представить новую информацию о заметке, вы создадите новую таблицу под названием note. (Вспомните наше единственное соглашение об именовании таблиц.) Таблица выглядит примерно так:
note_id | person_id | содержание | отметка времени |
1 | 1 | cool, a mini-blogging application! | 2019-01-06 22:17:54 |
2 | 1 | this could be useful | 2019-01-08 22:17:54 |
3 | 1 | well, sort of useful |
2019-03-06 22:17:54 |
4 | 2 | i’m going to make really profound observations | 2019-01-07 22:17:54 |
5 | 2 | maybe they’ll be more obvious than i thought | 2019-02-06 22:17:54 |
6 | 3 | has anyone seen my easter eggs? | 2019-01-07 22:47:54 |
7 | 3 | i’m really late delivering these! | 2019-04-06 22:17:54 |
Обратите внимание, что, как и таблица person, таблица note имеет уникальный идентификатор note_id,который является первичным ключом для таблицы note. Одна вещь, которая не является очевидной, - это включение значения person_id в таблицу. Для чего это используется? Это то, что создает отношение к таблице персон. В то время как note_id является первичным ключом для таблицы, person_id-это то, что известно как внешний ключ.
Внешний ключ дает каждой записи в таблице Примечаний первичный ключ записи человека, с которым она связана. Используя это, SQLAlchemy может собрать все заметки, связанные с каждым человеком, подключив его.первичный ключ person_id к заметке.внешний ключ person_id, создающий отношения.
Преимущества
Разбив набор данных на две таблицы и введя понятие внешнего ключа, вы сделали данные немного более сложными для осмысления, вы решили недостатки представления одной таблицы. SQLAlchemy поможет вам довольно легко кодировать повышенную сложность.
Эти данные больше не являются избыточными в базе данных. Существует только одна запись человека для каждого человека, которого вы хотите сохранить в базе данных. Это сразу же решает проблему хранения и значительно упрощает задачи технического обслуживания.
Если бы Пасхальный кролик все еще хотел изменить имена, то вам нужно было бы изменить только одну строку в таблице person, и все остальное, связанное с этой строкой (например, таблица заметок), немедленно воспользовалось бы этим изменением.
Именование столбцов является более последовательным и значимым. Поскольку данные персон и заметок существуют в отдельных таблицах, временная метка создания и обновления может быть названа последовательно в обеих таблицах, поскольку нет никакого конфликта для имен между таблицами.
Кроме того, вам больше не придется создавать перестановки каждой строки для новых отношений "один ко многим", которые вы, возможно, захотите представить. Возьмем пример наших детей и phone_numbers из более ранних работ. Для реализации этого процесса потребуются таблицы child и phone_number. Каждая таблица будет содержать внешний ключ person_id, связывающий ее обратно с таблицей person.
Используя SQLAlchemy, данные, которые вы получите обратно из приведенных выше таблиц, будут более полезны сразу же, так как то, что вы получите, - это объект для каждой строки person. Этот объект имеет именованные атрибуты, эквивалентные столбцам в таблице. Одним из таких атрибутов является список Python, содержащий связанные объекты note.
Недостатки
Там, где подход грубой силы был проще для понимания, концепция внешних ключей и отношений делает мышление о данных несколько более абстрактным. Эта абстракция должна быть продумана для каждого отношения, которое вы устанавливаете между таблицами.
Использование отношений означает принятие обязательств по использованию системы баз данных. Это еще один инструмент для установки, изучения и обслуживания выше и за пределами приложения, которое фактически использует данные.
Модели SQLAlchemy
Чтобы использовать две вышеприведенные таблицы и отношения между ними, вам нужно будет создать модели SQLAlchemy, которые будут знать обе таблицы и отношения между ними. Вот модель SQLAlchemy Person из части 2, обновленная, чтобы включить связь с коллекцией заметок:
class Person(db.Model): __tablename__ = 'person' person_id = db.Column(db.Integer, primary_key=True) lname = db.Column(db.String(32)) 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)' )
Строки 1-8 из приведенного выше класса Python выглядят точно так же, как и то, что вы создали ранее в части 2. Строки с 9 по 16 создают новый атрибут в классе Person, который называется notes. Эти новые атрибуты notes определяются в следующих строках кода:
- Строка 9: как и другие атрибуты класса, эта строка создает новый атрибут notes и устанавливает его равным экземпляру объекта db.отношения. Этот объект создает отношение, которое вы добавляете в класс Person, и создается со всеми параметрами, определенными в следующих строках.
- Строка 10: строковый параметр 'Note' определяет класс SQLAlchemy, с которым будет связан класс Person. Класс Note еще не определен, поэтому здесь это строка. Это прямая ссылка и помогает справиться с проблемами, которые может вызвать порядок определений, когда требуется что-то, что не определено ранее в коде. Строка 'Note' позволяет классу Person найти класс Note во время выполнения, то есть после определения как Person, так и Note.
- Строка 11: параметр backref= 'person' сложнее. Он создает то, что известно как обратная ссылка в объектах заметок. Каждый экземпляр объекта Note будет содержать атрибут person. Атрибут person ссылается на родительский объект, с которым связан конкретный экземпляр заметки. Наличие ссылки на родительский объект (в данном случае на человека) в дочернем коде может быть очень полезно, если ваш код повторяет заметки и должен включать информацию о родителе. Это происходит на удивление часто в коде отображения рендеринга.
- Строка 12: параметр cascade= 'all, delete, delete-orphan' определяет, как обращаться с экземплярами объектов note при внесении изменений в родительский экземпляр Person. Например, когда объект Person удаляется, SQLAlchemy создаст SQL, необходимый для удаления человека из базы данных. Кроме того, этот параметр указывает ему также удалить все связанные с ним экземпляры заметок. Подробнее об этих параметрах можно прочитать в документации по SQLAlchemy.
- Строка 13: параметр single_parent=True требуется, если параметр delete-orphan является частью предыдущего каскадного параметра. Это говорит SQLAlchemy не позволять осиротевшим экземплярам заметок (заметка без родительского объекта Person) существовать, потому что каждая заметка имеет одного родителя.
- Строка 14: the order_by= ' desc(Примечание.параметр timestamp) ' указывает SQLAlchemy, как сортировать экземпляры заметок, связанные с человеком. При извлечении объекта Person по умолчанию список атрибутов notes будет содержать объекты Notes в неизвестном порядке. SQLAlchemy desc(...) функция будет сортировать заметки в порядке убывания от самых новых к самым старым. Если бы эта строка была вместо order_by= ' Примечание.timestamp', SQLAlchemy будет по умолчанию использовать asc(...) функция и сортировка заметок в порядке возрастания, от самых старых до самых новых.
Теперь, когда ваша модель Person имеет новый атрибут notes, и это представляет отношение "один ко многим" для объектов Note, вам нужно будет определить модель SQLAlchemy для заметки:
class Note(db.Model): __tablename__ = 'note' note_id = db.Column(db.Integer, primary_key=True) person_id = db.Column(db.Integer, db.ForeignKey('person.person_id')) content = db.Column(db.String, nullable=False) timestamp = db.Column( db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow )
Класс Note определяет атрибуты, составляющие заметку, как показано в нашем примере таблицы базы данных заметок выше. Атрибуты определены здесь:
- Строка 1 создает класс Note, наследуемый от db.Модель, точно так же, как вы делали это раньше при создании класса Person.
- Строка 2 указывает классу, какую таблицу базы данных следует использовать для хранения объектов Примечаний.
- Строка 3 создает атрибут note_id, определяя его как целочисленное значение и как первичный ключ для объекта Note.
- Строка 4 создает атрибут person_id и определяет его как внешний ключ, связывающий класс Note с классом Person, использующим person.первичный ключ person_id. Это и есть человек.атрибут notes-это то, как SQLAlchemy знает, что делать при взаимодействии с объектами Person и Note.
- Строка 5 создает атрибут content, который содержит фактический текст заметки. Параметр nullable=False указывает, что можно создавать новые заметки, которые не содержат содержимого.
- Строка 6 создает атрибут timestamp, и точно так же, как класс Person, он содержит время создания или обновления для любого конкретного экземпляра заметки.
Инициализация базы данных
Теперь, когда вы обновили человека и создали модели заметок, вы будете использовать их для перестройки людей тестовой базы данных.дБ. Вы сделаете это, обновив build_database.py код из части 2. Вот как будет выглядеть этот код:
import os from datetime import datetime from config import db from models import Person, Note # Данные для инициализации базы данных PEOPLE = [ { "fname": "Doug", "lname": "Farrell", "notes": [ ("Cool, a mini-blogging application!", "2019-01-06 22:17:54"), ("This could be useful", "2019-01-08 22:17:54"), ("Well, sort of useful", "2019-03-06 22:17:54"), ], }, { "fname": "Kent", "lname": "Brockman", "notes": [ ( "I'm going to make really profound observations", "2019-01-07 22:17:54", ), ( "Maybe they'll be more obvious than I thought", "2019-02-06 22:17:54", ), ], }, { "fname": "Bunny", "lname": "Easter", "notes": [ ("Has anyone seen my Easter eggs?", "2019-01-07 22:47:54"), ("I'm really late delivering these!", "2019-04-06 22:17:54"), ], }, ] # Удалите файл базы данных, если он существует в данный момент if os.path.exists("people.db"): os.remove("people.db") # Создание базы данных db.create_all() # Выполните итерацию по структуре людей и заполните базу данных for person in PEOPLE: p = Person(lname=person.get("lname"), fname=person.get("fname")) # Добавьте заметки для этого человека for note in person.get("notes"): content, timestamp = note p.notes.append( Note( content=content, timestamp=datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S"), ) ) db.session.add(p) db.session.commit()
Приведенный выше код был взят из части 2, с некоторыми изменениями, чтобы создать отношения "один ко многим" между человеком и заметкой. Вот обновленные или новые строки, добавленные в код:
- Строка 4 была обновлена для импорта класса заметок, определенного ранее.
- Строки с 7 по 39 содержат обновленный словарь людей, содержащий наши личные данные, а также список заметок, связанных с каждым человеком. Эти данные будут вставлены в базу данных.
- Строки с 49 по 61 повторяют словарь людей, получая каждого человека по очереди и используя его для создания объекта Person.
- Строка 53 повторяется над человеком.список заметок, получая каждую заметку по очереди.
- Строка 54 распаковывает содержимое и метку времени из каждого кортежа заметок.
- Линия от 55 до 60 создает объект и добавляет его внимание к человеку, Примечания, используя стр. Примечания.добавлять().
- Строка 61 добавляет объект Person p в сеанс базы данных.
- Строка 63 фиксирует все действия в сеансе в базе данных. Именно в этот момент все данные записываются в таблицы персон и заметок в people.файл базы данных БД.
Вы можете видеть, что работа с коллекцией notes в экземпляре объекта Person p похожа на работу с любым другим списком в Python. SQLAlchemy заботится о лежащей в основе информации об отношениях "один ко многим", когда БД.сессия.вызов commit () выполнен.
Например, точно так же, как экземпляр Person имеет свое поле первичного ключа person_id, инициализированное SQLAlchemy, когда он фиксируется в базе данных, экземпляры Note будут иметь свои поля первичного ключа инициализированными. Кроме того, Note foreign key person_id также будет инициализирован значением первичного ключа экземпляра Person, с которым он связан.
Вот примерный экземпляр объекта Person перед БД.сессия.commit () в виде псевдокода:
Person ( person_id = None lname = 'Farrell' fname = 'Doug' timestamp = None notes = [ Note ( note_id = None person_id = None content = 'Cool, a mini-blogging application!' timestamp = '2019-01-06 22:17:54' ), Note ( note_id = None person_id = None content = 'This could be useful' timestamp = '2019-01-08 22:17:54' ), Note ( note_id = None person_id = None content = 'Well, sort of useful' timestamp = '2019-03-06 22:17:54' ) ] )
Вот пример объекта Person после db.session.commit():
Person ( person_id = 1 lname = 'Farrell' fname = 'Doug' timestamp = '2019-02-02 21:27:10.336' notes = [ Note ( note_id = 1 person_id = 1 content = 'Cool, a mini-blogging application!' timestamp = '2019-01-06 22:17:54' ), Note ( note_id = 2 person_id = 1 content = 'This could be useful' timestamp = '2019-01-08 22:17:54' ), Note ( note_id = 3 person_id = 1 content = 'Well, sort of useful' timestamp = '2019-03-06 22:17:54' ) ] )
Важное различие между ними заключается в том, что первичный ключ объекта Person и Note был инициализирован. Компонент database engine позаботился об этом, поскольку объекты были созданы из-за функции автоматического увеличения первичных ключей, описанной в части 2.
Кроме того, внешний ключ person_id во всех экземплярах заметки был инициализирован для ссылки на его родителя. Это происходит из-за порядка, в котором создаются объекты Person и Note в базе данных.
SQLAlchemy осознает связь между человеком и объектами заметок. Когда объект Person фиксируется в таблице базы данных person, SQLAlchemy получает значение первичного ключа person_id. Это значение используется для инициализации значения внешнего ключа person_id в объекте Note перед его фиксацией в базе данных.
SQLAlchemy берет на себя эту работу по ведению домашнего хозяйства базы данных из-за информации, которую вы передали, когда человек.атрибут notes был инициализирован с помощью БД.отношение(...) объект.
Кроме того, сам человек.атрибут timestamp был инициализирован текущей меткой времени.
Запускаем программу build_database.py программа из командной строки (в виртуальной среде будет заново создавать базу данных с новыми дополнениями, подготавливая ее к использованию с веб-приложением. Эта командная строка перестроит базу данных:
python build_database.py
В чем дело: build_database.py служебная программа не выводит никаких сообщений, если она работает успешно. Если он создает исключение, то на экране будет выведена ошибка.
Обновление REST API
Вы обновили модели SQLAlchemy и использовали их для обновления людей.база данных БД. Теперь пришло время обновить REST API, чтобы обеспечить доступ к новой информации notes. Вот REST API, который вы построили в части 2:
Действие | http-глагол | url-путь | Описание |
---|---|---|---|
Создание | post | /api/people | определяет уникальный url для создания нового человека |
Чтение | get | /api/people | определяет уникальный url для чтения коллекции людей |
Чтение | get | /api/people/{person_id} | определяет уникальный url для чтения конкретного человека по person_id |
Обновление | put | /api/people/{person_id} | определяет уникальный url для обновления существующего человека по person_id |
Удаление | delete | /api/people/{person_id} | определяет уникальный url для удаления существующего человека по person_id |
Приведенный выше REST API предоставляет HTTP URL-пути к коллекциям вещей и к самим вещам. Вы можете получить список людей или взаимодействовать с одним человеком из этого списка людей. Этот стиль пути уточняет то, что возвращается слева направо, становясь более детализированным по мере продвижения.
Вы продолжите этот шаблон слева направо, чтобы получить более детализированный доступ к коллекциям заметок. Вот расширенный REST API, который вы создадите для предоставления заметок в веб-приложение mini-blog:
Действие | http-глагол | url-путь | Описание |
---|---|---|---|
Создание | post | /api/people/{person_id}/notes | URL для создания новой заметки |
Чтение | get | /api/people/{person_id}/notes/{note_id} | URL-адрес, чтобы прочитать один один человек Примечание |
Обновление | put | api/people/{person_id}/notes/{note_id} | URL-адрес для обновления одной заметки одного человека |
Удаление | delete | api/people/{person_id}/notes/{note_id} | URL для удаления единственной заметки одного человека |
Чтение | get | /api/notes | URL-адрес, чтобы получить все заметки для всех людей, отсортированных по заметкам.отметка времени |
В части notes API REST есть два варианта по сравнению с соглашением, используемым в разделе people:
- Там нет URL-адреса, определенного для получения всех заметок, связанных с человеком, только URL-адрес, чтобы получить одну заметку. Это сделало бы REST API в некотором смысле завершенным, но веб-приложение, которое вы создадите позже, не нуждается в этой функциональности. Поэтому он был оставлен в стороне.
- Существует включение последнего URL / api / notes. Это удобный метод, созданный для веб-приложения. Он будет использоваться в мини-блоге на главной странице, чтобы показать все заметки в системе. Нет способа легко получить эту информацию, используя разработанный стиль патчинга REST API, поэтому этот ярлык был добавлен.
Как и в части 2, REST API настроен в swagger.файл yml.
Примечание:
Идея разработки REST API с путем, который становится все более и более детализированным при перемещении слева направо, очень полезна. Такое мышление может помочь прояснить отношения между различными частями базы данных. Просто имейте в виду, что существуют реалистичные ограничения на то, как далеко вниз по иерархической структуре должен быть взят этот вид дизайна.
Например, что делать, если объект Note имеет собственную коллекцию, что-то вроде комментариев к заметкам. Используя текущие идеи дизайна, это приведет к URL-адресу, который будет выглядеть примерно так: /api / people/{person_id} / notes/{note_id} / comments / {comment_id}
Нет никакого практического предела этому виду дизайна, но есть один для полезности. При фактическом использовании в реальных приложениях длинный многоуровневый URL-адрес, подобный этому, вряд ли когда-либо понадобится. Более распространенным шаблоном является получение списка промежуточных объектов (например, заметок), а затем использование отдельной точки входа API для получения одного комментария для варианта использования приложения.
Реализация API
С обновленным REST API, определенным в swagger.файл yml, вам нужно будет обновить реализацию, предоставляемую модулями Python. Это означает обновление существующих файлов модулей, например models.py и еще people.py, и создание нового файла модуля под названием notes.py чтобы реализовать поддержку заметок в расширенном REST API.
Обновление ответа JSON
Целью REST API является получение полезных данных JSON из базы данных. Теперь, когда вы обновили SQLAlchemy Person и создали модели заметок,вам также нужно будет обновить модели схемы Marshmallow. Как вы помните из части 2, Marshmallow-это модуль, который переводит объекты SQLAlchemy в объекты Python, подходящие для создания строк JSON.
Обновленные и вновь созданные схемы зефира находятся в models.py модуль, который описан ниже, и выглядит примерно так:
class PersonSchema(ma.ModelSchema): class Meta: model = Person sqla_session = db.session notes = fields.Nested('PersonNoteSchema', default=[], many=True) class PersonNoteSchema(ma.ModelSchema): """ This class exists to get around a recursion issue """ note_id = fields.Int() person_id = fields.Int() content = fields.Str() timestamp = fields.Str() class NoteSchema(ma.ModelSchema): class Meta: model = Note sqla_session = db.session person = fields.Nested('NotePersonSchema', default=None) class NotePersonSchema(ma.ModelSchema): """ Этот класс существует, чтобы обойти проблему рекурсии """ person_id = fields.Int() lname = fields.Str() fname = fields.Str() timestamp = fields.Str()
Есть некоторые интересные вещи, происходящие в приведенных выше определениях. Класс PersonSchema имеет одну новую запись: атрибут notes, определенный в строке 5. Это определяет его как вложенное отношение к PersonNoteSchema. По умолчанию он будет пустым списком, если в связи SQLAlchemy notes ничего не присутствует. Параметр many=True указывает, что это отношение "один ко многим", поэтому Marshmallow сериализует все связанные заметки.
Класс PersonNoteSchema определяет, как выглядит объект заметки, когда Marshmallow сериализует список заметок. В NoteSchema определяет, что объект с SQLAlchemy отметить, выглядит с точки зрения алтея. Обратите внимание,что у него есть атрибут person. Этот атрибут происходит из базы данных SQLAlchemy.отношение(...) параметр определения backref= 'person'. Определение Person Marshmallow является вложенным, но поскольку оно не имеет параметра many=True, то подключается только один человек.
Класс NotePersonSchema определяет, что вложено в NoteSchema.атрибут человека.
Примечание:
Возможно, Вам будет интересно, почему класс PersonSchema имеет свой собственный уникальный класс PersonNoteSchema для определения атрибута коллекции notes. Точно так же класс NoteSchema имеет свой собственный уникальный класс NotePersonSchema для определения атрибута person. Вам может быть интересно, может ли класс PersonSchema быть определен таким образом:
class PersonSchema(ma.ModelSchema): class Meta: model = Person sqla_session = db.session notes = fields.Nested('NoteSchema', default=[], many=True)Кроме того, разве класс NoteSchema не может быть определен с помощью PersonSchema для определения атрибута person? Определение класса, подобное этому, будет ссылаться на другое, и это вызывает ошибку рекурсии в Marshmallow, поскольку он будет циклически переходить от PersonSchema к NoteSchema, пока не закончится пространство стека. Использование уникальных ссылок на схемы нарушает рекурсию и позволяет этому виду вложенности работать.
People
Теперь, когда у вас есть схемы для работы с отношениями "один ко многим" между человеком и заметкой, вам нужно обновить схему. person.py и создать то же самое note.py модули для реализации рабочего REST API.
В чем дело people.py модуль нуждается в двух изменениях. Во-первых, необходимо импортировать класс Note вместе с классом Person в верхней части модуля. Тогда только read_one(person_id) должен измениться, чтобы справиться с отношением. Эта функция будет выглядеть следующим образом:
def read_one(person_id): """ Эта функция отвечает на запрос /api / people / {person_id} с одним подходящим человеком из людей :param person_id: Id человека, которого нужно найти :return: личность, совпадающая с идентификатором """ # Постройте начальный запрос person = ( Person.query.filter(Person.person_id == person_id) .outerjoin(Note) .one_or_none() ) # Мы нашли человека? if person is not None: # Сериализуйте данные для ответа person_schema = PersonSchema() data = person_schema.dump(person).data return data # В противном случае, нет, я не нашел этого человека else: abort(404, f"Person not found for Id: {person_id}")
Единственное отличие-это строка 12: .внешнее соединение (Примечание). Внешнее соединение (left outer join в терминах SQL) необходимо для случая, когда пользователь приложения создал новый объект person, который не имеет связанных с ним заметок. Внешнее соединение гарантирует, что SQL-запрос вернет объект person, даже если нет строк заметок для объединения.
В начале этой статьи вы увидели, как данные о персонах и заметках могут быть представлены в одной плоской таблице, и все недостатки этого подхода. Вы также увидели преимущества разбиения этих данных на две таблицы, person и note, с отношением между ними.
До сих пор мы работали с данными как с двумя различными, но связанными элементами в базе данных. Но теперь, когда вы действительно собираетесь использовать данные, мы, по сути, хотим, чтобы эти данные были объединены обратно. Это то, что делает соединение с базой данных. Он объединяет данные из двух таблиц вместе, используя отношение первичного ключа к внешнему ключу.
Соединение-это своего рода логическая операция, поскольку оно возвращает данные только в том случае, если в обеих таблицах есть данные для объединения. Если, например, строка person существует, но не имеет связанной строки note, то нет ничего, к чему можно было бы присоединиться, поэтому ничего не возвращается. Это не то, что вы хотите для read_one(person_id).
Вот тут-то и пригодится внешнее соединение. Это своего рода логическое выражение или операция. Он возвращает данные человека, даже если нет никаких связанных данных заметки для объединения. Это поведение, которое вы хотите, чтобы read_one (person_id) обрабатывал случай недавно созданного объекта Person, который еще не имеет заметок.
Вы можете увидеть полный текст people.py в репозитарии статьи.
Notes
Вы создадите модуль notes.py чтобы реализовать весь код на Python, связанные с новыми Примечание соответствующие определения API-интерфейса REST. Во многих отношениях это работает так же, как и people.py модуль, за исключением того, что он должен обрабатывать как person_id, так и note_id, как определено в swagger.конфигурационный файл yml. В качестве примера приведем read_one(person_id, note_id):
def read_one(person_id, note_id): """ Эта функция отвечает на запрос о /api/people/{person_id}/notes/{note_id} с одной соответствующей note для связанного лица :param person_id: Id of person the note is related to :param note_id: Id of the note :return: json string of note contents """ # Запросите эту заметку в базе данных note = ( Note.query.join(Person, Person.person_id == Note.person_id) .filter(Person.person_id == person_id) .filter(Note.note_id == note_id) .one_or_none() ) # Была ли найдена запись? if note is not None: note_schema = NoteSchema() data = note_schema.dump(note).data return data # В противном случае, нет, я не нашел эту запись else: abort(404, f"Note not found for Id: {note_id}")
Наиболее интересными частями приведенного выше кода являются строки с 12 по 17:
- Строка 13 начинается запрос с запиской объектов SQLAlchemy и присоединяется к соответствующему лицу с SQLAlchemy объект сравнения person_id от лица и внимание.
- Строка 14 фильтрует результат вплоть до объектов Заметки, в которых есть человек.person_id равен переданному параметру person_id.
- Линия 15 фильтров, результат в дальнейшем объект, обратите внимание, что есть внимание.note_id равен переданному в note_id параметру.
- Строка 16 возвращает объект Note, если он найден, или None, если не найдено ничего, соответствующего параметрам.
Вы можете проверить полную версию notes.py-да.
Обновленный интерфейс Swagger
Пользовательский интерфейс Swagger был обновлен действием обновления swagger.файл yml и создание реализаций конечной точки URL-адреса. Ниже приведен скриншот обновленного пользовательского интерфейса, показывая разделе Примечаний с GET /api-интерфейс/люди/{person_id}/Примечания/{note_id} расширенный:
Веб-Приложение Для Мини-Блога
Веб-приложение было существенно изменено, чтобы показать его новое назначение в качестве приложения для мини-блогов. Оно состоит из трех страниц:
- Главная страница (localhost:5000/), на которой отображаются все сообщения блога (заметки), отсортированные от самых новых до самых старых
- Страница people (localhost: 5000 / people), которая показывает всех людей в системе, отсортированных по фамилии, а также позволяет пользователю создать нового человека и обновить или удалить существующего
- Страница заметок (localhost: 5000 / people / {person_id} / notes), которая показывает все заметки, связанные с человеком, отсортированные от самых новых до самых старых, а также позволяет пользователю создать новую заметку и обновить или удалить существующую
Навигация
На каждой странице приложения есть две кнопки:
- Кнопка "Домой" - переход к главному экрану.
- Кнопка люди - переход на экран /people, показывая всех людей в базе данных.
Эти две кнопки присутствуют на каждом экране приложения как способ вернуться к исходной точке.
Домашняя страница
Ниже приведен скриншот домашней страницы, показывающий инициализированное содержимое базы данных:
Функциональность этой страницы работает следующим образом:
- Двойной щелчок по имени человека приведет пользователя на страницу /people / {person_id}, где раздел редактора будет заполнен именем и фамилией человека, а кнопки обновления и сброса будут включены.
- Двойной щелчок по заметке человека приведет пользователя на страницу /people / {person_id} / notes/{note_id}, где раздел редактора заполнен содержимым заметки и включены кнопки обновления и сброса.
Страница people
Ниже приведен скриншот страницы люди, показывающий людей в инициализированной базе данных:
Функциональность этой страницы работает следующим образом:
- При однократном щелчке по имени пользователя раздел редактора страницы заполняется его именем и фамилией, отключается кнопка Создать и включаются кнопки Обновить и удалить.
- Двойной щелчок по имени человека приведет к переходу на страницы заметок для этого человека.
Функциональность редактора работает следующим образом:
- Если поля Имя и фамилия пусты, то включаются кнопки Создать и сбросить. Ввод нового имени в поля и нажатие кнопки Создать приведет к созданию нового человека, обновлению базы данных и повторному отображению таблицы под редактором. Нажав кнопку Сброс, вы очистите поля редактора.
- Если в полях " Имя " и "фамилия" есть данные, пользователь перемещается сюда, дважды щелкнув имя пользователя на главном экране. В этом случае включены кнопки Обновить, удалить и сбросить. Изменение имени или фамилии и нажатие кнопки Обновить приведет к обновлению базы данных и повторному отображению таблицы под редактором. Нажатие кнопки Удалить приведет к удалению пользователя из базы данных и повторному отображению таблицы.
Notes Page
Ниже приведен скриншот страницы заметок, показывающий заметки для человека в инициализированной базе данных:
Функциональность этой страницы работает следующим образом:
- Один щелчок по заметке заполнит раздел редактора страницы содержимым заметок, отключив кнопку Создать и включив кнопки Обновить и удалить.
- Все остальные функции этой страницы находятся в разделе редактор.
Функциональность редактора работает следующим образом:
- Если поле содержание заметки пусто, то кнопки Создать и сбросить включены. Ввод новой заметки в поле и нажатие кнопки Создать приведет к созданию новой заметки, обновлению базы данных и повторному отображению таблицы под редактором. Нажав кнопку Сброс, вы очистите поля редактора.
- Если поле заметка содержит данные, пользователь перемещается сюда, дважды щелкнув заметку пользователя на главном экране. В этом случае включены кнопки Обновить, удалить и сбросить. Изменение примечания и нажатие кнопки Обновить приведет к обновлению базы данных и повторному отображению таблицы под редактором. Нажатие кнопки Удалить приведет к удалению заметки из базы данных и повторному отображению таблицы.
Веб-приложение
Эта статья в первую очередь посвящена тому, как использовать SQLAlchemy для создания связей в базе данных и как расширить REST API, чтобы использовать преимущества этих связей. Таким образом, код для веб-приложения не привлек особого внимания. Когда вы смотрите на код веб-приложения, обратите внимание на следующие функции:
- Каждая страница приложения представляет собой полностью сформированное одностраничное веб-приложение.
- Каждая страница приложения управляется JavaScript в соответствии со стилем делегирования ответственности MVC (Model/View/Controller).
- HTML, который создает страницы, использует преимущества функции наследования Jinja2.
- Создание жестко закодированной таблицы JavaScript было заменено использованием руля управления.шаблонизатор на JS.
- Временная метка, формирующаяся во всех таблицах, определяется моментом.JS.
Вы можете найти следующий код в репозитории для этой статьи:
- HTML для веб-приложения
- CSS для веб-приложения
- JavaScript для веб-приложения
Весь пример кода для этой статьи доступен в репозитории GitHub для этой статьи. Здесь содержится весь код, связанный с этой статьей, включая весь код веб-приложения.
Вывод
Поздравляем вас с тем, что вы узнали в этой статье! Знание того, как строить и использовать отношения баз данных, дает вам мощный инструмент для решения многих сложных проблем. Есть и другие отношения, помимо примера "один ко многим" из этой статьи. Другие распространенные из них-один к одному, многие ко многим и многие к одному. Все они имеют место в вашем поясе инструментов, и SQLAlchemy может помочь вам справиться с ними всеми!
Для получения дополнительной информации о базах данных вы можете ознакомиться с этими учебными пособиями. Вы также можете настроить Flask для использования SQLAlchemy. Вы можете проверить Model-View-Controller (MVC) более подробную информацию о шаблоне, используемом в коде JavaScript веб-приложения.
В части 4 этой серии вы сосредоточитесь на файлах HTML, CSS и JavaScript, используемых для создания веб-приложения.