23 KiB
SQL (реляційні) бази даних
FastAPI не вимагає від вас використовувати SQL (реляційну) базу даних. Але ви можете скористатися будь-якою базою даних, яку забажаєте.
Тут ми розглянемо приклад з SQLModel.
SQLModel побудовано поверх SQLAlchemy та Pydantic. Її створив той самий автор, що і FastAPI, як ідеальну пару для застосунків FastAPI, яким потрібні SQL бази даних.
/// tip | Порада
Ви можете використовувати будь-яку іншу бібліотеку для SQL або NoSQL баз (інколи їх називають "ORMs"), FastAPI нічого не нав’язує. 😎
///
Оскільки SQLModel базується на SQLAlchemy, ви можете легко використовувати будь-яку базу даних, яку підтримує SQLAlchemy (а отже й SQLModel), наприклад:
- PostgreSQL
- MySQL
- SQLite
- Oracle
- Microsoft SQL Server тощо
У цьому прикладі ми використаємо SQLite, оскільки вона зберігається в одному файлі, а Python має вбудовану підтримку. Тож ви можете скопіювати цей приклад і запустити «як є».
Пізніше, для вашого продакшн-застосунку, ви можете перейти на сервер бази даних, наприклад PostgreSQL.
/// tip | Порада
Існує офіційний генератор проєкту з FastAPI та PostgreSQL, включно з фронтендом та іншими інструментами: https://github.com/fastapi/full-stack-fastapi-template
///
Це дуже простий і короткий навчальний посібник. Якщо ви хочете вивчити бази даних загалом, SQL або більш просунуті можливості, зверніться до документації SQLModel.
Встановіть SQLModel
Спочатку переконайтеся, що ви створили віртуальне оточення{.internal-link target=_blank}, активували його та встановили sqlmodel:
$ pip install sqlmodel
---> 100%
Створіть застосунок з однією моделлю
Спершу створимо найпростішу версію застосунку з однією моделлю SQLModel.
Потім нижче покращимо безпеку і гнучкість за допомогою кількох моделей. 🤓
Створіть моделі
Імпортуйте SQLModel і створіть модель бази даних:
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[1:11] hl[7:11] *}
Клас Hero дуже схожий на модель Pydantic (фактично, під капотом це і є модель Pydantic).
Є кілька відмінностей:
-
table=Trueкаже SQLModel, що це «таблична модель» - вона має представляти таблицю в SQL базі даних, а не просто «модель даних» (як будь-який інший звичайний клас Pydantic). -
Field(primary_key=True)каже SQLModel, щоid- це первинний ключ у SQL базі даних (більше про первинні ключі в SQL див. у документації SQLModel).Примітка: Ми використовуємо
int | Noneдля поля первинного ключа, щоб у Python-коді можна було створити об’єкт безid(id=None), припускаючи, що база даних згенерує його під час збереження. SQLModel розуміє, щоidнадасть база даних, і визначає стовпець як ненульовийINTEGERу схемі бази даних. Докладніше див. документацію SQLModel про первинні ключі. -
Field(index=True)каже SQLModel створити SQL-індекс для цього стовпця, що дозволить швидше виконувати пошук у базі даних під час читання даних, відфільтрованих за цим стовпцем.SQLModel знатиме, що оголошене як
strстане SQL-стовпцем типуTEXT(абоVARCHAR, залежно від бази).
Створіть рушій
engine SQLModel (під капотом це engine SQLAlchemy) - це те, що утримує з’єднання з базою даних.
У вас має бути єдиний об’єкт engine для всього коду, що під’єднується до тієї самої бази.
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[14:18] hl[14:15,17:18] *}
Використання check_same_thread=False дозволяє FastAPI використовувати ту саму базу SQLite у різних потоках. Це необхідно, адже один запит може використати понад один потік (наприклад, у залежностях).
Не переймайтеся, завдяки структурі коду ми далі забезпечимо одну сесію SQLModel на запит, власне цього і прагне check_same_thread.
Створіть таблиці
Додамо функцію, яка використовує SQLModel.metadata.create_all(engine), щоб створити таблиці для всіх «табличних моделей».
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[21:22] hl[21:22] *}
Створіть залежність сесії
Session зберігає об’єкти в пам’яті та відстежує зміни у даних, а потім використовує engine для взаємодії з базою даних.
Ми створимо залежність FastAPI з yield, яка надаватиме нову Session для кожного запиту. Це й гарантує, що ми використовуємо одну сесію на запит. 🤓
Далі створимо залежність Annotated - SessionDep, щоб спростити решту коду, який її використовуватиме.
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[25:30] hl[25:27,30] *}
Створюйте таблиці під час запуску
Ми створимо таблиці бази під час запуску застосунку.
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[32:37] hl[35:37] *}
Тут ми створюємо таблиці на події запуску застосунку.
У продакшні ви, ймовірно, використаєте міграційний скрипт, який виконується перед стартом застосунку. 🤓
/// tip | Порада
SQLModel матиме утиліти міграцій-обгортки над Alembic, але поки що ви можете використовувати Alembic напряму.
///
Створіть героя
Оскільки кожна модель SQLModel також є моделлю Pydantic, ви можете використовувати її в тих самих анотаціях типів, що і моделі Pydantic.
Наприклад, якщо ви оголосите параметр типу Hero, він буде прочитаний з JSON-тіла.
Так само ви можете оголосити її як тип, що повертається функції, і форма даних з’явиться в автоматичному UI документації API.
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[40:45] hl[40:45] *}
Тут ми використовуємо залежність SessionDep (Session), щоб додати нового Hero до екземпляра Session, закомітити зміни до бази, оновити дані в hero, а потім повернути його.
Читання героїв
Ми можемо читати Hero з бази даних, використовуючи select(). Ми можемо додати limit і offset для пагінації результатів.
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[48:55] hl[51:52,54] *}
Читання одного героя
Ми можемо прочитати одного Hero.
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[58:63] hl[60] *}
Видалення героя
Ми також можемо видалити Hero.
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[66:73] hl[71] *}
Запуск застосунку
Ви можете запустити застосунок:
$ fastapi dev main.py
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
Потім перейдіть до інтерфейсу /docs, ви побачите, що FastAPI використовує ці моделі для документування API, а також для серіалізації та валідації даних.
Оновіть застосунок кількома моделями
Тепер трохи відрефакторимо застосунок, щоб підвищити безпеку та гнучкість.
Якщо подивитися на попередній варіант, у UI видно, що наразі клієнт може сам обирати id для створюваного Hero. 😱
Цього допускати не можна - вони можуть перезаписати id, який уже призначений у БД. Визначення id має виконувати бекенд або база даних, а не клієнт.
Крім того, ми створюємо для героя secret_name, але наразі повертаємо його всюди - це не дуже «секретно»... 😅
Виправимо це, додавши кілька додаткових моделей. Ось де SQLModel засяє. ✨
Створіть кілька моделей
У SQLModel будь-який клас моделі з table=True - це таблична модель.
А будь-який клас моделі без table=True - це модель даних; це, по суті, просто моделі Pydantic (з парою невеликих доповнень). 🤓
З SQLModel ми можемо використовувати успадкування, щоб уникати дублювання полів у всіх випадках.
HeroBase - базовий клас
Почнімо з моделі HeroBase, яка містить усі спільні поля для всіх моделей:
nameage
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:9] hl[7:9] *}
Hero - «таблична модель»
Потім створімо Hero, фактичну «табличну модель», з додатковими полями, яких немає в інших моделях завжди:
idsecret_name
Оскільки Hero успадковується від HeroBase, вона також містить поля, оголошені в HeroBase, тож усі поля Hero такі:
idnameagesecret_name
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:14] hl[12:14] *}
HeroPublic - публічна «модель даних»
Далі створимо модель HeroPublic - це та, що буде повертатися клієнтам API.
Вона має ті самі поля, що й HeroBase, отже не включатиме secret_name.
Нарешті, особистості наших героїв захищено! 🥷
Вона також перевизначає id: int. Роблячи це, ми укладаємо контракт з клієнтами API, що id завжди буде присутній і буде типу int (ніколи не None).
/// tip | Порада
Наявність у моделі відповіді гарантії, що значення завжди доступне і завжди int (не None), дуже корисна для клієнтів API - вони можуть писати значно простіший код, маючи цю визначеність.
Також автоматично згенеровані клієнти матимуть простіші інтерфейси, тож розробникам, які взаємодіють з вашим API, буде значно зручніше. 😎
///
Усі поля в HeroPublic такі самі, як у HeroBase, з id, оголошеним як int (не None):
idnameage
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:18] hl[17:18] *}
HeroCreate - «модель даних» для створення героя
Тепер створимо модель HeroCreate - вона валідуватиме дані, отримані від клієнтів.
Вона має ті ж поля, що й HeroBase, а також secret_name.
Тепер, коли клієнти створюють нового героя, вони надсилають secret_name, він зберігається в базі даних, але ці секретні імена не повертаються клієнтам у API.
/// tip | Порада
Так слід поводитися з паролями. Приймайте їх, але не повертайте в API.
Також потрібно хешувати значення паролів перед збереженням, ніколи не зберігайте їх у відкритому вигляді.
///
Поля HeroCreate:
nameagesecret_name
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:22] hl[21:22] *}
HeroUpdate - «модель даних» для оновлення героя
У попередній версії у нас не було способу оновлювати героя, але тепер, маючи кілька моделей, ми можемо це зробити. 🎉
«Модель даних» HeroUpdate дещо особлива: вона має всі ті самі поля, що й для створення нового героя, але всі поля необов’язкові (усі мають значення за замовчуванням). Таким чином, під час оновлення героя ви можете надіслати лише ті поля, які хочете змінити.
Оскільки поля фактично змінюються (тип тепер включає None і значення за замовчуванням - None), нам потрібно оголосити їх заново.
Насправді немає потреби успадковуватися від HeroBase, бо ми перевизначаємо всі поля. Я залишу успадкування для послідовності, але це не обов’язково. Це радше питання смаку. 🤷
Поля HeroUpdate:
nameagesecret_name
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:28] hl[25:28] *}
Створення з HeroCreate і повернення HeroPublic
Тепер, коли в нас кілька моделей, ми можемо оновити частини застосунку, що їх використовують.
У запиті ми отримуємо «модель даних» HeroCreate, і з неї створюємо «табличну модель» Hero.
Ця нова «таблична модель» Hero міститиме поля, надіслані клієнтом, а також матиме id, згенерований базою даних.
Далі ми повертаємо з функції цю ж «табличну модель» Hero. Але оскільки ми оголошуємо response_model як «модель даних» HeroPublic, FastAPI використає HeroPublic для валідації та серіалізації даних.
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[56:62] hl[56:58] *}
/// tip | Порада
Тепер ми використовуємо response_model=HeroPublic замість анотації типу, що повертається -> HeroPublic, тому що значення, яке ми повертаємо, насправді - не HeroPublic.
Якби ми оголосили -> HeroPublic, ваш редактор і лінтер справедливо зауважили б, що ви повертаєте Hero, а не HeroPublic.
Оголошуючи це в response_model, ми кажемо FastAPI зробити свою справу, не втручаючись в анотації типів та підказки від вашого редактора й інших інструментів.
///
Читання героїв з HeroPublic
Можемо зробити те саме, що й раніше, щоб читати Hero, знову використовуємо response_model=list[HeroPublic], щоб гарантувати коректну валідацію та серіалізацію даних.
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[65:72] hl[65] *}
Читання одного героя з HeroPublic
Ми можемо прочитати одного героя:
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[75:80] hl[77] *}
Оновлення героя з HeroUpdate
Ми можемо оновити героя. Для цього використовуємо HTTP-операцію PATCH.
У коді ми отримуємо dict з усіма даними, які надіслав клієнт, - лише з тими даними, які надіслав клієнт, виключаючи будь-які значення, присутні лише як значення за замовчуванням. Для цього використовуємо exclude_unset=True. Це головний трюк. 🪄
Потім використовуємо hero_db.sqlmodel_update(hero_data), щоб оновити hero_db даними з hero_data.
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[83:93] hl[83:84,88:89] *}
Знову видалення героя
Видалення героя майже не змінилося.
Ми не задовольнимо бажання відрефакторити все в цьому місці. 😅
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[96:103] hl[101] *}
Знову запустіть застосунок
Ви можете знову запустити застосунок:
$ fastapi dev main.py
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
Якщо ви перейдете до UI /docs, побачите, що він оновився і більше не очікуватиме отримати id від клієнта під час створення героя тощо.
Підсумок
Ви можете використовувати SQLModel для взаємодії з SQL базою даних і спростити код за допомогою «моделей даних» та «табличних моделей».
Багато чого ще можна дізнатися в документації SQLModel, там є розширений міні-навчальний посібник з використання SQLModel з FastAPI. 🚀