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

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

В част 2 от тази серия добавихте възможността за запазване на промените, направени чрез REST API, към база данни с помощта на SQLAlchemy и научихте как да сериализирате тези данни за REST API с помощта на Marshmallow. Свързването на REST API към база данни, така че приложението да може да прави промени в съществуващите данни и да създава нови данни, е страхотно и прави приложението много по-полезно и стабилно.

Това обаче е само част от силата, която базата данни предлага. Още по-мощна функция е R част отRDBMS системи:отношения . В база данни връзката е способността за свързване на две или повече таблици заедно по смислен начин. В тази статия ще научите как да внедрите взаимоотношения и да превърнете своя Person база данни в уеб приложение за мини блогове.

В тази статия ще научите:

  • Защо повече от една таблица в база данни е полезна и важна
  • Как таблиците са свързани една с друга
  • Как SQLAlchemy може да ви помогне да управлявате взаимоотношенията
  • Как връзките ви помагат да изградите приложение за мини-блог

За кого е тази статия

Част 1 от тази серия ви преведе през изграждането на REST API, а част 2 ви показа как да свържете този REST API към база данни.

Тази статия разширява допълнително вашия пояс с инструменти за програмиране. Ще научите как да създавате йерархични структури от данни, представени като връзки един към много от SQLAlchemy. Освен това ще разширите REST API, който вече сте изградили, за да осигурите поддръжка на CRUD (Създаване, четене, актуализиране и изтриване) за елементите в тази йерархична структура.

Уеб приложението, представено в част 2, ще има своите HTML и JavaScript файлове, модифицирани по основни начини, за да създаде по-пълно функционално приложение за мини-блог. Можете да прегледате окончателната версия на кода от част 2 в хранилището на GitHub за тази статия.

Изчакайте, докато започнете да създавате взаимоотношения и вашето приложение за мини-блог!



Допълнителни зависимости

Няма нови зависимости на Python извън това, което се изискваше за статията от част 2. Въпреки това, вие ще използвате два нови модула на JavaScript в уеб приложението, за да направите нещата по-лесни и по-последователни. Двата модула са следните:

  1. Handlebars.js е шаблонна машина за JavaScript, подобно на Jinja2 за Flask.
  2. Moment.js е модул за синтактичен анализ и форматиране на дата и час, който улеснява показването на времеви марки в UTC.

Не е нужно да изтегляте нито едно от тях, тъй като уеб приложението ще ги получи директно от Cloudflare CDN (мрежа за доставка на съдържание), както вече правите за модула jQuery.



Разширени данни за хората за блогове

В част 2, People данните съществуваха като речник в build_database.py Python код. Това е, което използвахте, за да попълните базата данни с някои първоначални данни. Ще промените People структура от данни, за да даде на всеки човек списък с бележки, свързани с него. Новият People структурата на данните ще изглежда така:

# Data to initialize database with
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"),
        ],
    },
]

Всеки човек в People речникът вече включва ключ, наречен notes , който е свързан със списък, съдържащ кортежи от данни. Всеки кортеж в notes списък представлява една бележка съдържащ съдържанието и времева марка. Отпечатъците за време се инициализират (а не се създават динамично), за да демонстрират подреждането по-късно в REST API.

Всеки отделен човек е свързан с множество бележки, а всяка отделна бележка е свързана само с един човек. Тази йерархия от данни е известна като връзка един към много, където един родителски обект е свързан с много дъщерни обекти. Ще видите как тази връзка един към много се управлява в базата данни с SQLAlchemy.



Подход с груба сила

Базата данни, която сте изградили, съхранява данните в таблица, а таблицата е двуизмерен масив от редове и колони. Може ли People речник по-горе да бъде представен в една таблица от редове и колони? Може да бъде по следния начин във вашето person таблица на базата данни. За съжаление включването на всички действителни данни в примера създава лента за превъртане за таблицата, както ще видите по-долу:

person_id lname fname timestamp content note_timestamp
1 Фарел Дъг 2018-08-08 21:16:01 Готино, приложение за мини-блог! 2019-01-06 22:17:54
2 Фарел Дъг 2018-08-08 21:16:01 Това може да е полезно 2019-01-08 22:17:54
3 Фарел Дъг 2018-08-08 21:16:01 Е, нещо полезно 2019-03-06 22:17:54
4 Брокман Кент 2018-08-08 21:16:01 Ще направя наистина задълбочени наблюдения 2019-01-07 22:17:54
5 Брокман Кент 2018-08-08 21:16:01 Може би ще бъдат по-очевидни, отколкото си мислех 2019-02-06 22:17:54
6 Великден Зайче 2018-08-08 21:16:01 Някой виждал ли е моите великденски яйца? 2019-01-07 22:47:54
7 Великден Зайче 2018-08-08 21:16:01 Наистина закъснявам с доставянето им! 2019-04-06 22:17:54

Таблицата по-горе наистина ще работи. Всички данни са представени и един човек е свързан с колекция от различни бележки.


Предимства

Концептуално, горната структура на таблицата има предимството, че е относително лесна за разбиране. Можете дори да докажете, че данните могат да бъдат запазени в плосък файл вместо в база данни.

Поради двуизмерната структура на таблицата можете да съхранявате и използвате тези данни в електронна таблица. Електронните таблици са били въведени в услуга като съхранение на данни доста малко.



Недостатъци

Въпреки че горната структура на таблицата би работила, тя има някои реални недостатъци.

За да се представи колекцията от бележки, всички данни за всяко лице се повтарят за всяка уникална бележка, поради което данните за лицето са излишни. Това не е толкова голяма работа за вашите лични данни, тъй като няма толкова много колони. Но представете си, ако човек имаше много повече колони. Дори при големи дискови устройства това може да се превърне в проблем за съхранение, ако имате работа с милиони редове данни.

Наличието на излишни данни като тази може да доведе до проблеми с поддръжката с течение на времето. Например, какво ще стане, ако Великденският заек реши, че промяната на името е добра идея. За да направите това, всеки запис, съдържащ името на Великденския заек, трябва да бъде актуализиран, за да се поддържат данните последователни. Този вид работа срещу базата данни може да доведе до несъответствие на данните, особено ако работата се извършва от лице, изпълняващо SQL заявка на ръка.

Именуването на колони става неудобно. В таблицата по-горе има timestamp колона, използвана за проследяване на времето за създаване и актуализиране на човек в таблицата. Вие също искате да имате подобна функционалност за времето за създаване и актуализиране на бележка, но тъй като timestamp вече се използва, измислено име на note_timestamp се използва.

Ами ако искате да добавите допълнителни връзки един към много към person маса? Например, за да включите децата или телефонните номера на даден човек. Всеки човек може да има няколко деца и няколко телефонни номера. Това може да се направи сравнително лесно за People на Python речник по-горе, като добавите children и phone_numbers ключове с нови списъци, съдържащи данните.

Представяйки обаче тези нови връзки един към много във вашето person таблицата на базата данни по-горе става значително по-трудна. Всяка нова връзка едно към много увеличава драстично броя на редовете, необходими за представянето й за всеки отделен запис в дъщерните данни. Освен това проблемите, свързани с излишъка на данни, стават все по-големи и по-трудни за справяне.

И накрая, данните, които ще получите от горната структура на таблицата, няма да са много Pythonic:това ще бъде просто голям списък от списъци. SQLAlchemy няма да може да ви помогне много, защото връзката не е налице.




Подход към релационна база данни

Въз основа на това, което видяхте по-горе, става ясно, че опитът да се представи дори умерено сложен набор от данни в една таблица става неуправляем доста бързо. Като се има предвид това, каква алтернатива предлага базата данни? Това е мястото, където R част отRDBMS бази данни влизат в игра. Представянето на взаимоотношения премахва посочените по-горе недостатъци.

Вместо да се опитват да представят йерархични данни в една таблица, данните се разбиват на множество таблици с механизъм за свързването им една с друга. Таблиците са разбити по линиите за събиране, така че за вашите People речник по-горе, това означава, че ще има таблица, представяща хората и друга, представляваща бележки. Това връща оригиналния ви person таблица, която изглежда така:

person_id lname 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

За да представите информацията за новата бележка, ще създадете нова таблица, наречена note . (Запомнете нашата конвенция за именуване на таблици в единствено число.) Таблицата изглежда така:

note_id person_id content timestamp
1 1 Готино, приложение за мини-блог! 2019-01-06 22:17:54
2 1 Това може да е полезно 2019-01-08 22:17:54
3 1 Е, нещо полезно 2019-03-06 22:17:54
4 2 Ще направя наистина задълбочени наблюдения 2019-01-07 22:17:54
5 2 Може би ще бъдат по-очевидни, отколкото си мислех 2019-02-06 22:17:54
6 3 Някой виждал ли е моите великденски яйца? 2019-01-07 22:47:54
7 3 Наистина закъснявам с доставянето им! 2019-04-06 22:17:54

Забележете това, като person таблица, note таблицата има уникален идентификатор, наречен note_id , който е първичен ключ за note маса. Едно нещо, което не е очевидно, е включването на person_id стойност в таблицата. Това за какво се използва? Това е, което създава връзката с person маса. Докато note_id е първичният ключ за таблицата, person_id е това, което е известно като външен ключ.

Външният ключ дава всеки запис в note таблица първичния ключ на person запис, с който е свързан. Използвайки това, SQLAlchemy може да събере всички бележки, свързани с всеки човек, като свърже person.person_id първичен ключ към note.person_id външен ключ, създаване на връзка.


Предимства

Чрез разбиването на набора от данни на две таблици и въвеждането на концепцията за външен ключ, вие направихте данните малко по-сложни за мислене, разрешихте недостатъците на представянето на една таблица. SQLAlchemy ще ви помогне да кодирате увеличената сложност сравнително лесно.

Данните вече не са излишни в базата данни. Има само един запис на човек за всеки човек, който искате да съхраните в базата данни. Това незабавно решава проблема със съхранението и драстично опростява проблемите с поддръжката.

Ако Великденското зайче все още иска да промени имената, тогава ще трябва да промените само един ред в person таблица и всичко друго, свързано с този ред (като note таблица) веднага ще се възползва от промяната.

Именуването на колоните е по-последователно и смислено. Тъй като данните за лица и бележки съществуват в отделни таблици, клеймото за време за създаване и актуализиране може да бъде наименувано последователно и в двете таблици, тъй като няма конфликт за имена в различните таблици.

Освен това вече няма да се налага да създавате пермутации на всеки ред за нови връзки един към много, които може да искате да представите. Вземете нашите children и phone_numbers пример от по-рано. Прилагането на това ще изисква child и phone_number маси. Всяка таблица ще съдържа външен ключ person_id свързвайки го обратно с person таблица.

Използвайки SQLAlchemy, данните, които ще получите от горните таблици, биха били по-полезни, тъй като това, което ще получите, е обект за всеки ред с лица. Този обект има именувани атрибути, еквивалентни на колоните в таблицата. Един от тези атрибути е списък на Python, съдържащ свързаните бележки.



Недостатъци

Когато подходът на грубата сила беше по-прост за разбиране, концепцията за външни ключове и връзки прави мисленето за данните малко по-абстрактно. Тази абстракция трябва да се мисли за всяка връзка, която установявате между таблици.

Използването на връзки означава ангажиране с използването на система от база данни. Това е друг инструмент за инсталиране, научаване и поддръжка над и извън приложението, което всъщност използва данните.




SQLAlchemy модели

За да използвате двете таблици по-горе и връзката между тях, ще трябва да създадете SQLAlchemy модели, които са наясно с двете таблици и връзката между тях. Ето SQLAlchemy Person модел от част 2, актуализиран, за да включва връзка с колекция от notes :

 1class Person(db.Model):
 2    __tablename__ = 'person'
 3    person_id = db.Column(db.Integer, primary_key=True)
 4    lname = db.Column(db.String(32))
 5    fname = db.Column(db.String(32))
 6    timestamp = db.Column(
 7        db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow
 8    )
 9    notes = db.relationship(
10        'Note',
11        backref='person',
12        cascade='all, delete, delete-orphan',
13        single_parent=True,
14        order_by='desc(Note.timestamp)'
15    )

Редове от 1 до 8 от горния клас на Python изглеждат точно като това, което сте създали преди в част 2. Редове от 9 до 16 създават нов атрибут в Person клас, наречен notes . Тези нови notes атрибути се дефинира в следните редове код:

  • Ред 9: Подобно на другите атрибути на класа, този ред създава нов атрибут, наречен notes и го задава равен на екземпляр на обект, наречен db.relationship . Този обект създава връзката, която добавяте към Person клас и се създава с всички параметри, дефинирани в редовете, които следват.

  • Ред 10: Параметърът на низа 'Note' дефинира класа SQLAlchemy, който Person клас ще бъде свързан с. Note class все още не е дефиниран, поради което тук е низ. Това е препратка и помага за справяне с проблеми, които редът на дефинициите може да причини, когато е необходимо нещо, което не е дефинирано до по-късно в кода. 'Note' низ позволява Person клас, за да намерите Note клас по време на изпълнение, което е след двете Person и Note са определени.

  • Ред 11: backref='person' параметърът е по-сложен. Той създава това, което е известно като обратна препратка в Note обекти. Всеки екземпляр на Note обектът ще съдържа атрибут, наречен person . person атрибут препраща към родителския обект, който конкретен Note екземпляр е свързан с. Наличие на препратка към родителския обект (person в този случай) в детето може да бъде много полезно, ако кодът ви претърпява бележки и трябва да включва информация за родителя. Това се случва изненадващо често в кода за изобразяване на дисплея.

  • Ред 12: cascade='all, delete, delete-orphan' параметърът определя как да се третират екземпляри на бележки, когато се правят промени в родителския Person екземпляр. Например, когато Person обектът е изтрит, SQLAlchemy ще създаде SQL необходимия за изтриване на Person от базата данни. Освен това, този параметър му казва да изтрие и всички Note случаи, свързани с него. Можете да прочетете повече за тези опции в документацията на SQLAlchemy.

  • Ред 13: single_parent=True параметърът е задължителен, ако delete-orphan е част от предишната cascade параметър. Това казва на SQLAlchemy да не разрешава осиротяла Note екземпляри (a Note без родител Person обект) да съществува, защото всяка Note има самотен родител.

  • Ред 14: order_by='desc(Note.timestamp)' параметърът казва на SQLAlchemy как да сортира Note екземпляри, свързани с Person . Когато Person обектът се извлича, по подразбиране notes списъкът с атрибути ще съдържа Note обекти в неизвестен ред. SQLAlchemy desc(...) функцията ще сортира бележките в низходящ ред от най-новите към най-старите. Ако вместо това този ред беше order_by='Note.timestamp' , SQLAlchemy ще използва по подразбиране asc(...) функция и сортирайте бележките във възходящ ред, от най-старите към най-новите.

Сега, когато вашето Person моделът има новите notes атрибут и това представлява връзката едно към много към Note обекти, ще трябва да дефинирате модел на SQLAlchemy за Note :

 1class Note(db.Model):
 2    __tablename__ = 'note'
 3    note_id = db.Column(db.Integer, primary_key=True)
 4    person_id = db.Column(db.Integer, db.ForeignKey('person.person_id'))
 5    content = db.Column(db.String, nullable=False)
 6    timestamp = db.Column(
 7        db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow
 8    )

Note класът дефинира атрибутите, съставляващи бележка, както се вижда в нашата примерна note таблица на базата данни отгоре. Атрибутите са дефинирани тук:

  • Линия 1 създава Note клас, наследяващ от db.Model , точно както направихте преди, когато създавахте Person клас.

  • Ред 2 казва на класа каква таблица на базата данни да използва за съхраняване на Note обекти.

  • Ред 3 създава note_id атрибут, дефинирайки го като целочислена стойност и като първичен ключ за Note обект.

  • Реда 4 създава person_id атрибут и го дефинира като външен ключ, свързан с Note клас към Person клас с помощта на person.person_id първичен ключ. Това и Person.notes атрибут, са как SQLAlchemy знае какво да прави, когато взаимодейства с Person и Note обекти.

  • Ред 5 създава content атрибут, който съдържа действителния текст на бележката. nullable=False параметърът показва, че е добре да създавате нови бележки, които нямат съдържание.

  • Ред 6 създава timestamp атрибут и точно като Person клас, това съдържа времето за създаване или актуализиране за всеки конкретен Note пример.



Инициализирайте базата данни

Сега, когато актуализирахте Person и създаде Note модели, ще ги използвате, за да изградите отново тестовата база данни people.db . Ще направите това, като актуализирате build_database.py код от част 2. Ето как ще изглежда кодът:

 1import os
 2from datetime import datetime
 3from config import db
 4from models import Person, Note
 5
 6# Data to initialize database with
 7PEOPLE = [
 8    {
 9        "fname": "Doug",
10        "lname": "Farrell",
11        "notes": [
12            ("Cool, a mini-blogging application!", "2019-01-06 22:17:54"),
13            ("This could be useful", "2019-01-08 22:17:54"),
14            ("Well, sort of useful", "2019-03-06 22:17:54"),
15        ],
16    },
17    {
18        "fname": "Kent",
19        "lname": "Brockman",
20        "notes": [
21            (
22                "I'm going to make really profound observations",
23                "2019-01-07 22:17:54",
24            ),
25            (
26                "Maybe they'll be more obvious than I thought",
27                "2019-02-06 22:17:54",
28            ),
29        ],
30    },
31    {
32        "fname": "Bunny",
33        "lname": "Easter",
34        "notes": [
35            ("Has anyone seen my Easter eggs?", "2019-01-07 22:47:54"),
36            ("I'm really late delivering these!", "2019-04-06 22:17:54"),
37        ],
38    },
39]
40
41# Delete database file if it exists currently
42if os.path.exists("people.db"):
43    os.remove("people.db")
44
45# Create the database
46db.create_all()
47
48# Iterate over the PEOPLE structure and populate the database
49for person in PEOPLE:
50    p = Person(lname=person.get("lname"), fname=person.get("fname"))
51
52    # Add the notes for the person
53    for note in person.get("notes"):
54        content, timestamp = note
55        p.notes.append(
56            Note(
57                content=content,
58                timestamp=datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S"),
59            )
60        )
61    db.session.add(p)
62
63db.session.commit()

Кодът по-горе идва от част 2, с няколко промени за създаване на връзката един към много между Person и Note . Ето актуализираните или новите редове, добавени към кода:

  • Реда 4 е актуализиран за импортиране на Note клас, дефиниран по-рано.

  • Редове от 7 до 39 съдържат актуализираните PEOPLE речник, съдържащ нашите лични данни, заедно със списъка с бележки, свързани с всеки човек. Тези данни ще бъдат вмъкнати в базата данни.

  • Редове от 49 до 61 повторете над PEOPLE речник, получавайки всеки person на свой ред и да го използва за създаване на Person обект.

  • Линия 53 итерира над person.notes списък, получавайки всяка note на свой ред.

  • Линия 54 разопакова content и timestamp от всяка note кортеж.

  • Ред 55 до 60 създава Note обект и го добавя към колекцията от бележки за хора, използвайки p.notes.append() .

  • Линия 61 добавя Person обект p към сесията на базата данни.

  • Линия 63 записва цялата дейност в сесията в базата данни. В този момент всички данни се записват на person и note таблици в people.db файл с база данни.

Можете да видите, че работите с notes колекция в Person екземпляр на обект p е точно като работата с всеки друг списък в Python. SQLAlchemy се грижи за основната информация за връзката един към много, когато db.session.commit() повикването е извършено.

Например, точно като Person екземплярът има полето за първичен ключ person_id инициализиран от SQLAlchemy, когато е ангажиран с базата данни, екземпляри на Note ще имат инициализирани полета за първичен ключ. В допълнение, Note външен ключ person_id също ще бъде инициализиран със стойността на първичния ключ на Person например е свързано с.

Ето примерен екземпляр на Person обект преди db.session.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 обектите са инициализирани. Двигателят на базата данни се погрижи за това, тъй като обектите бяха създадени поради функцията за автоматично увеличаване на първичните ключове, разгледана в част 2.

Освен това person_id външен ключ във всички Note екземпляри са инициализирани, за да се позовават на своя родител. Това се случва поради реда, в който Person и Note обектите се създават в базата данни.

SQLAlchemy е наясно с връзката между Person и Note обекти. Когато Person обектът се предава на person таблица на база данни, SQLAlchemy получава person_id стойност на първичния ключ. Тази стойност се използва за инициализиране на стойността на външния ключ на person_id в Note обект, преди да бъде закаран в базата данни.

SQLAlchemy се грижи за работата по поддържането на тази база данни поради информацията, която сте предали, когато Person.notes атрибутът е инициализиран с db.relationship(...) обект.

В допълнение, Person.timestamp атрибутът е инициализиран с текущото времеви печат.

Изпълняване на build_database.py програма от командния ред (във виртуалната среда ще създаде отново базата данни с новите допълнения, като я подготви за използване с уеб приложението. Този команден ред ще изгради отново базата данни:

$ python build_database.py

build_database.py помощната програма не извежда никакви съобщения, ако работи успешно. Ако хвърли изключение, тогава на екрана ще бъде отпечатана грешка.



Актуализиране на REST API

Вие актуализирахте SQLAlchemy моделите и ги използвахте за актуализиране на people.db база данни. Сега е време да актуализирате REST API, за да предоставите достъп до информацията за новите бележки. Ето 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, който ще създадете, за да предоставите бележки към уеб приложението за мини-блог:

Действие 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, за да получите всички бележки за всички хора, сортирани по note.timestamp

Има два варианта на notes част от REST API в сравнение с конвенцията, използвана в people раздел:

  1. Няма дефиниран URL адрес за получаване на всички notes свързан с човек, само URL за получаване на една бележка. Това би направило REST API завършен по някакъв начин, но уеб приложението, което ще създадете по-късно, не се нуждае от тази функционалност. Следователно е пропуснато.

  2. Има включване на последния URL /api/notes . Това е удобен метод, създаден за уеб приложението. Ще се използва в мини-блога на началната страница за показване на всички бележки в системата. There isn’t a way to get this information readily using the REST API pathing style as designed, so this shortcut has been added.

As in Part 2, the REST API is configured in the swagger.yml file.

Забележка:

The idea of designing a REST API with a path that gets more and more granular as you move from left to right is very useful. Thinking this way can help clarify the relationships between different parts of a database. Just be aware that there are realistic limits to how far down a hierarchical structure this kind of design should be taken.

For example, what if the Note object had a collection of its own, something like comments on the notes. Using the current design ideas, this would lead to a URL that went something like this:/api/people/{person_id}/notes/{note_id}/comments/{comment_id}

There is no practical limit to this kind of design, but there is one for usefulness. In actual use in real applications, a long, multilevel URL like that one is hardly ever needed. A more common pattern is to get a list of intervening objects (like notes) and then use a separate API entry point to get a single comment for an application use case.



Implement the API

With the updated REST API defined in the swagger.yml file, you’ll need to update the implementation provided by the Python modules. This means updating existing module files, like models.py and people.py , and creating a new module file called notes.py to implement support for Notes in the extended REST API.


Update Response JSON

The purpose of the REST API is to get useful JSON data out of the database. Now that you’ve updated the SQLAlchemy Person and created the Note models, you’ll need to update the Marshmallow schema models as well. As you may recall from Part 2, Marshmallow is the module that translates the SQLAlchemy objects into Python objects suitable for creating JSON strings.

The updated and newly created Marshmallow schemas are in the models.py module, which are explained below, and look like this:

 1class PersonSchema(ma.ModelSchema):
 2    class Meta:
 3        model = Person
 4        sqla_session = db.session
 5    notes = fields.Nested('PersonNoteSchema', default=[], many=True)
 6
 7class PersonNoteSchema(ma.ModelSchema):
 8    """
 9    This class exists to get around a recursion issue
10    """
11    note_id = fields.Int()
12    person_id = fields.Int()
13    content = fields.Str()
14    timestamp = fields.Str()
15
16class NoteSchema(ma.ModelSchema):
17    class Meta:
18        model = Note
19        sqla_session = db.session
20    person = fields.Nested('NotePersonSchema', default=None)
21
22class NotePersonSchema(ma.ModelSchema):
23    """
24    This class exists to get around a recursion issue
25    """
26    person_id = fields.Int()
27    lname = fields.Str()
28    fname = fields.Str()
29    timestamp = fields.Str()

There are some interesting things going on in the above definitions. The PersonSchema class has one new entry:the notes attribute defined in line 5. This defines it as a nested relationship to the PersonNoteSchema . It will default to an empty list if nothing is present in the SQLAlchemy notes връзка. The many=True parameter indicates that this is a one-to-many relationship, so Marshmallow will serialize all the related notes .

The PersonNoteSchema class defines what a Note object looks like as Marshmallow serializes the notes списък. The NoteSchema defines what a SQLAlchemy Note object looks like in terms of Marshmallow. Notice that it has a person attribute. This attribute comes from the SQLAlchemy db.relationship(...) definition parameter backref='person' . The person Marshmallow definition is nested, but because it doesn’t have the many=True parameter, there is only a single person свързани.

The NotePersonSchema class defines what is nested in the NoteSchema.person attribute.

Забележка:

You might be wondering why the PersonSchema class has its own unique PersonNoteSchema class to define the notes collection attribute. By the same token, the NoteSchema class has its own unique NotePersonSchema class to define the person attribute. You may be wondering whether the PersonSchema class could be defined this way:

class PersonSchema(ma.ModelSchema):
    class Meta:
        model = Person
        sqla_session = db.session
    notes = fields.Nested('NoteSchema', default=[], many=True)

Additionally, couldn’t the NoteSchema class be defined using the PersonSchema to define the person attribute? A class definition like this would each refer to the other, and this causes a recursion error in Marshmallow as it will cycle from PersonSchema to NoteSchema until it runs out of stack space. Using the unique schema references breaks the recursion and allows this kind of nesting to work.



People

Now that you’ve got the schemas in place to work with the one-to-many relationship between Person and Note , you need to update the person.py and create the note.py modules in order to implement a working REST API.

The people.py module needs two changes. The first is to import the Note class, along with the Person class at the top of the module. Then only read_one(person_id) needs to change in order to handle the relationship. That function will look like this:

 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    # Build the initial query
10    person = (
11        Person.query.filter(Person.person_id == person_id)
12        .outerjoin(Note)
13        .one_or_none()
14    )
15
16    # Did we find a person?
17    if person is not None:
18
19        # Serialize the data for the response
20        person_schema = PersonSchema()
21        data = person_schema.dump(person).data
22        return data
23
24    # Otherwise, nope, didn't find that person
25    else:
26        abort(404, f"Person not found for Id: {person_id}")

The only difference is line 12:.outerjoin(Note) . An outer join (left outer join in SQL terms) is necessary for the case where a user of the application has created a new person object, which has no notes related to it. The outer join ensures that the SQL query will return a person object, even if there are no note rows to join with.

At the start of this article, you saw how person and note data could be represented in a single, flat table, and all of the disadvantages of that approach. You also saw the advantages of breaking that data up into two tables, person and note , with a relationship between them.

Until now, we’ve been working with the data as two distinct, but related, items in the database. But now that you’re actually going to use the data, what we essentially want is for the data to be joined back together. This is what a database join does. It combines data from two tables together using the primary key to foreign key relationship.

A join is kind of a boolean and operation because it only returns data if there is data in both tables to combine. If, for example, a person row exists but has no related note row, then there is nothing to join, so nothing is returned. This isn’t what you want for read_one(person_id) .

This is where the outer join comes in handy. It’s a kind of boolean or операция. It returns person data even if there is no associated note data to combine with. This is the behavior you want for read_one(person_id) to handle the case of a newly created Person object that has no notes yet.

You can see the complete people.py in the article repository.



Notes

You’ll create a notes.py module to implement all the Python code associated with the new note related REST API definitions. In many ways, it works like the people.py module, except it must handle both a person_id and a note_id as defined in the swagger.yml configuration file. As an example, here is read_one(person_id, note_id) :

 1def read_one(person_id, note_id):
 2    """
 3    This function responds to a request for
 4    /api/people/{person_id}/notes/{note_id}
 5    with one matching note for the associated person
 6
 7    :param person_id:       Id of person the note is related to
 8    :param note_id:         Id of the note
 9    :return:                json string of note contents
10    """
11    # Query the database for the note
12    note = (
13        Note.query.join(Person, Person.person_id == Note.person_id)
14        .filter(Person.person_id == person_id)
15        .filter(Note.note_id == note_id)
16        .one_or_none()
17    )
18
19    # Was a note found?
20    if note is not None:
21        note_schema = NoteSchema()
22        data = note_schema.dump(note).data
23        return data
24
25    # Otherwise, nope, didn't find that note
26    else:
27        abort(404, f"Note not found for Id: {note_id}")

The interesting parts of the above code are lines 12 to 17:

  • Line 13 begins a query against the Note SQLAlchemy objects and joins to the related Person SQLAlchemy object comparing person_id from both Person and Note .
  • Line 14 filters the result down to the Note objects that has a Person.person_id equal to the passed in person_id parameter.
  • Line 15 filters the result further to the Note object that has a Note.note_id equal to the passed in note_id parameter.
  • Line 16 returns the Note object if found, or None if nothing matching the parameters is found.

You can check out the complete notes.py .




Updated Swagger UI

The Swagger UI has been updated by the action of updating the swagger.yml file and creating the URL endpoint implementations. Below is a screenshot of the updated UI showing the Notes section with the GET /api/people/{person_id}/notes/{note_id} expanded:



Mini-Blogging Web Application

The web application has been substantially changed to show its new purpose as a mini-blogging application. It has three pages:

  1. The home page (localhost:5000/ ) , which shows all of the blog messages (notes) sorted from newest to oldest

  2. The people page (localhost:5000/people ) , which shows all the people in the system, sorted by last name, and also allows the user to create a new person and update or delete an existing one

  3. The notes page (localhost:5000/people/{person_id}/notes ) , which shows all the notes associated with a person, sorted from newest to oldest, and also allows the user to create a new note and update or delete an existing one


Navigation

There are two buttons on every page of the application:

  1. The Home button will navigate to the home screen.
  2. The People button navigates to the /people screen, showing all people in the database.

These two buttons are present on every screen in the application as a way to get back to a starting point.



Home Page

Below is a screenshot of the home page showing the initialized database contents:

The functionality of this page works like this:

  • Double-clicking on a person’s name will take the user to the /people/{person_id} page, with the editor section filled in with the person’s first and last names and the update and reset buttons enabled.

  • Double-clicking on a person’s note will take the user to the /people/{person_id}/notes/{note_id} page, with the editor section filled in with the note’s contents and the Update and Reset buttons enabled.



People Page

Below is a screenshot of the people page showing the people in the initialized database:

The functionality of this page works like this:

  • Single-clicking on a person’s name will populate the editor section of the page with the person’s first and last name, disabling the Create button, and enabling the Update and Delete бутони.

  • Double clicking on a person’s name will navigate to the notes pages for that person.

The functionality of the editor works like this:

  • If the first and last name fields are empty, the Create and Reset buttons are enabled. Entering a new name in the fields and clicking Create will create a new person and update the database and re-render the table below the editor. Clicking Reset will clear the editor fields.

  • If the first and last name fields have data, the user navigated here by double-clicking the person’s name from the home screen. In this case, the Update , Delete , and Reset buttons are enabled. Changing the first or last name and clicking Update will update the database and re-render the table below the editor. Clicking Delete will remove the person from the database and re-render the table.



Notes Page

Below is a screenshot of the notes page showing the notes for a person in the initialized database:

The functionality of this page works like this:

  • Single-clicking on a note will populate the editor section of the page with the notes content, disabling the Create button, and enabling the Update and Delete бутони.

  • All other functionality of this page is in the editor section.

The functionality of the editor works like this:

  • If the note content field is empty, then the Create and Reset buttons are enabled. Entering a new note in the field and clicking Create will create a new note and update the database and re-render the table below the editor. Clicking Reset will clear the editor fields.

  • If the note field has data, the user navigated here by double-clicking the person’s note from the home screen. In this case, the Update , Delete , and Reset buttons are enabled. Changing the note and clicking Update will update the database and re-render the table below the editor. Clicking Delete will remove the note from the database and re-render the table.



Web Application

This article is primarily focused on how to use SQLAlchemy to create relationships in the database, and how to extend the REST API to take advantage of those relationships. As such, the code for the web application didn’t get much attention. When you look at the web application code, keep an eye out for the following features:

  • Each page of the application is a fully formed single page web application.

  • Each page of the application is driven by JavaScript following an MVC (Model/View/Controller) style of responsibility delegation.

  • The HTML that creates the pages takes advantage of the Jinja2 inheritance functionality.

  • The hardcoded JavaScript table creation has been replaced by using the Handlebars.js templating engine.

  • The timestamp formating in all of the tables is provided by Moment.js.

You can find the following code in the repository for this article:

  • The HTML for the web application
  • The CSS for the web application
  • The JavaScript for the web application

All of the example code for this article is available in the GitHub repository for this article. This contains all of the code related to this article, including all of the web application code.




Заключение

Congratulations are in order for what you’ve learned in this article! Knowing how to build and use database relationships gives you a powerful tool to solve many difficult problems. There are other relationship besides the one-to-many example from this article. Other common ones are one-to-one, many-to-many, and many-to-one. All of them have a place in your toolbelt, and SQLAlchemy can help you tackle them all!

For more information about databases, you can check out these tutorials. You can also set up Flask to use SQLAlchemy. You can check out Model-View-Controller (MVC) more information about the pattern used in the web application JavaScript code.

In Part 4 of this series, you’ll focus on the HTML, CSS, and JavaScript files used to create the web application.

« Part 2:Database PersistencePart 3:Database RelationshipsPart 4:Simple Web Applications »

  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. SQL VIEW

  2. Урок за PL/SQL:Всичко, което трябва да знаете за PL/SQL

  3. Как да актуализирате колона въз основа на филтър на друга колона

  4. Съхранена процедура за получаване на състоянието на индексите във всички бази данни

  5. TVF с няколко изявления в Dynamics CRM