Модуль 32

ORM у FastAPI та SQLAlchemy

Ось готовий урок, створений спеціально для тебе у стилі David Malan — з енергією, аналогіями та фокусом на розумінні суті.


🎓 Урок CS50: ORM у FastAPI та SQLAlchemy

Привіт, друзі! Радий бачити вас. Сьогодні ми зазирнемо під капот сучасної веб-розробки та розберемося з магією, яка дозволяє Python спілкуватися з базами даних.

Тема нашого уроку: ORM (Object-Relational Mapping), і як ми це використовуємо у зв'язці FastAPI + SQLAlchemy.


1. 🔥 Вступ: проблема та мотивація

Уявіть, що ви приїхали в країну, мови якої абсолютно не знаєте. Скажімо, ви розмовляєте лише українською (це наш Python), а місцеві жителі — лише, наприклад, клінгонською (це наш SQL, мова баз даних).

Ви хочете замовити каву (отримати дані). У вас є два шляхи: 1. Вчити клінгонську. Вивчити всі ці SELECT, JOIN, WHERE, писати довгі рядки коду, де одна пропущена кома ламає все. 2. Найняти перекладача. Ви кажете перекладачеві українською: «Дай мені каву», а він сам розбирається, як це гаркнути клінгонською до офіціанта.

Риторичне запитання: Якщо ви хочете будувати складні архітектурні додатки, чи хочете ви витрачати 50% часу на написання нудних SQL-запитів вручну, ризикуючи зробити помилку в кожному рядку?

Звісно, ні. Ось тут на сцену виходить ORM.

Чому без цього не обійтись? Коли ми пишемо на FastAPI, ми оперуємо об'єктами (користувач, товар, замовлення). А база даних оперує таблицями та рядками. Це два різні світи. ORM — це міст, який дозволяє вам працювати з базою даних так, наче ви просто працюєте зі звичайними змінними Python.


2. 🧠 Теоретична база (без сухої академічності)

Давайте розкладемо це на пальцях.

Що таке ORM?

ORM (Object-Relational Mapping) — це технологія, яка пов'язує: * Клас у Python ➡️ Таблиця в БД * Екземпляр класу (об'єкт) ➡️ Рядок у таблиці * Атрибут класу ➡️ Колонка таблиці

Гравці на полі:

  1. FastAPI: Це наш веб-сервер. Він приймає запит від клієнта (наприклад, "Створити юзера").
  2. SQLAlchemy: Це і є наш "перекладач" (ORM). Найпопулярніша бібліотека для Python.
  3. База Даних (SQLite/PostgreSQL): Це склад, де лежать дані.

Як це працює "під капотом"?

Коли ви пишете на Python session.add(user), SQLAlchemy тихо формує SQL-запит INSERT INTO users... і відправляє його базі. Вам не треба знати синтаксис SQL, щоб це спрацювало.

🔑 Що запам'ятати обов'язково:

  • Engine (Двигун): Точка входу, підключення до бази.
  • Session (Сесія): Це ваша "транзакція". Уявіть це як кошик у супермаркеті. Ви кладете туди товари (зміни), і вони не куплені (не записані в БД), поки ви не підійдете до каси (не зробите commit).
  • Model (Модель): Клас Python, що описує таблицю.

Інтуїтивно: Модель — це креслення. База даних — це будівля. SQLAlchemy — це будівельна бригада.


3. 🧪 Приклади: Від "Hello World" до реальності

Зараз ми створимо міні-систему для зберігання нотаток.

Крок 1: Налаштування "Перекладача" (Database Setup)

Спершу нам треба сказати SQLAlchemy, де лежить база.

# database.py
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

# 1. Створюємо шлях до бази (для прикладу - простий файл SQLite)
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"

# 2. Запускаємо двигун (Engine)
# connect_args потрібен лише для SQLite
engine = create_engine(
    SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)

# 3. Фабрика сесій (те саме місце, де видають "кошики" для покупок)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

# 4. Базовий клас для наших моделей
Base = declarative_base()

Крок 2: Створення Моделі (Креслення)

Питання до вас: Як ви думаєте, якщо ми створимо клас Note у Python, чи з'явиться таблиця в базі автоматично просто так?

Відповідь: Ні! Нам треба явно попросити SQLAlchemy створити її на основі класу.

# models.py
from sqlalchemy import Column, Integer, String, Boolean
from database import Base

class Note(Base):
    __tablename__ = "notes"  # Як таблиця називатиметься в SQL

    id = Column(Integer, primary_key=True, index=True)
    title = Column(String, index=True)
    content = Column(String)
    is_active = Column(Boolean, default=True)

Крок 3: FastAPI + Використання (Магія)

Тепер поєднаємо це в main.py.

# main.py
from fastapi import FastAPI, Depends
from sqlalchemy.orm import Session
from database import SessionLocal, engine, Base
from models import Note

# Створюємо таблиці в базі (ось тут магія перетворення класу в SQL)
Base.metadata.create_all(bind=engine)

app = FastAPI()

# Dependency (Залежність): функція, яка видає нам сесію для роботи з БД
# і закриває її, коли запит завершено. Дуже важливо!
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

@app.post("/notes/")
def create_note(title: str, content: str, db: Session = Depends(get_db)):
    # 1. Створюємо об'єкт Python
    new_note = Note(title=title, content=content)

    # 2. Додаємо в "кошик" (сесію)
    db.add(new_note)

    # 3. Йдемо на касу (фіксуємо зміни в БД)
    db.commit()

    # 4. Оновлюємо об'єкт даними з БД (наприклад, щоб отримати ID)
    db.refresh(new_note)

    return new_note

Що тут відбулося? Ми не написали жодного рядка SQL коду. Ми просто створили об'єкт Note, і він зберігся у файлі test.db.


4. 🛠 Практична частина

Час забруднити руки кодом! Виконайте ці завдання по черзі:

  1. 🔹 Copy-Paste-Run: Скопіюйте код з прикладів у три файли (database.py, models.py, main.py), запустіть (uvicorn main:app --reload) і через Swagger UI (/docs) спробуйте створити нотатку.
  2. 🔹 Читання даних: Напишіть новий GET ендпоінт @app.get("/notes/"), який повертає список усіх нотаток.
    • Підказка: Вам знадобиться метод db.query(Note).all().
  3. 🔹 Зміна умов: Додайте до моделі Note нове поле author (тип String).
    • Увага: Якщо ви використовуєте SQLite, просто видаліть файл test.db і перезапустіть сервер, щоб таблиця перестворилась (це найпростіший шлях для початку).
  4. 🔹 Реальний кейс (Пошук): Зробіть так, щоб можна було знайти нотатку за id.
    • Підказка: db.query(Note).filter(Note.id == note_id).first().
  5. 🔹 А що, якщо... Що станеться, якщо у функції create_note видалити рядок db.commit()? Спробуйте. Чому сервер не видає помилку, але дані не з'являються при наступному запиті?

5. 💡 Мислення як у розробника

Як відрізнити новачка від профі в роботі з ORM?

❌ Помилка новачка: "Змішування понять"

Новачки часто плутають Pydantic-моделі (схеми для валідації вхідних даних FastAPI) і SQLAlchemy-моделі (відображення таблиць БД). * Pydantic — це охоронець на вході в клуб (перевіряє фейс-контроль даних). * SQLAlchemy — це архіваріус (кладе дані на полиці). Не намагайтеся запхати одне в інше. Використовуйте Pydantic для request/response, а ORM — для роботи всередині.

❌ Помилка новачка: N+1 проблема

Це коли ви робите цикл по об'єктах і для кожного робите окремий запит в базу. * Погано: Запитати список юзерів, а потім у циклі для кожного окремо питати його адресу. * Добре: Попросити ORM дістати юзерів ОДРАЗУ з їхніми адресами (через joinedload).

🧠 Як думає профі:

"Я не буду писати SQL вручну, поки це не стане критично для продуктивності". Профі знає: читабельність коду на Python важливіша за мікро-оптимізацію на старті.


6. 🧩 Підсумок

Отже, що ми сьогодні зробили: 1. Зрозуміли, що ORM — це наш особистий перекладач з Python на SQL. 2. Налаштували Session (кошик) та Engine (портал). 3. Створили таблицю, просто описавши Class. 4. Навчилися зберігати дані в базу, не написавши жодного INSERT.

Тепер ви вмієте: створювати бекенд на FastAPI, який має довготривалу пам'ять, а не забуває все після перезавантаження.

🚀 Тизер наступного уроку: Але що робити, якщо у нас є User, а у нього багато Notes? Як пов'язати ці дві таблиці між собою, щоб знати, чия це нотатка? На наступному уроці ми розберемо Relationships (Відносини) та Foreign Keys.

А поки що — практикуйтеся і не забувайте робити commit()! 😉