Модуль 22

SQLAlchemy з FastAPI: базове підключення

Ось готовий урок, написаний у стилі CS50, адаптований під тему SQLAlchemy та FastAPI.


🎓 CS50-style: SQLAlchemy та FastAPI — Будуємо міст до даних

Вітаю, друзі! 👋

Сьогодні ми не просто пишемо код. Сьогодні ми вчимося "дружити" дві різні галактики. З одного боку у нас FastAPI — швидкий, сучасний Python-фреймворк. З іншого — База Даних (наприклад, PostgreSQL), яка розмовляє суворою мовою SQL.

Як змусити їх розуміти одне одного без болю? Відповідь — SQLAlchemy. Поїхали!


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

Уявіть, що ви приїхали в країну, мови якої зовсім не знаєте (скажімо, Японію). Ви хочете замовити їжу в ресторані. У вас є два шляхи:

  1. Вчити японську (SQL) з нуля: Вивчати ієрогліфи, граматику, суфікси ввічливості, щоб сказати: "Будь ласка, принесіть мені суші з лососем, але без васабі". Якщо помилитесь в одному символі — отримаєте не суші, а рахунок за оренду ресторану.
  2. Використати ідеального перекладача (ORM): Ви кажете своєю рідною мовою (Python): "Хочу суші". Перекладач сам формулює ідеальну японську фразу і передає її шеф-кухарю.

Риторичне питання: Ви хочете витрачати час на написання довгих SQL-запитів вручну всередині Python-коду, ризикуючи помилками?

# Як це виглядає без ORM (Біль і страждання)
cursor.execute("INSERT INTO users (name, email) VALUES ('" + name + "', '" + email + "')")
# А якщо в змінній name буде лапка? Все зламається! 😱

Чому без цього не обійтись? SQLAlchemy — це ваш перекладач (ORM — Object-Relational Mapping). Вона дозволяє працювати з базами даних, використовуючи звичайні Python-класи та об'єкти. Ви змінюєте властивість об'єкта — SQLAlchemy оновлює запис у базі. Це безпечно, чисто і професійно.


2. 🧠 Теоретична база (без нудних лекцій)

Давайте розберемо, що відбувається "під капотом", використовуючи прості механічні аналогії.

Щоб підключити FastAPI до бази, нам потрібні три ключові компоненти:

1. Engine (Двигун) 🚂

Це серце нашої системи. Двигун знає, де знаходиться база (її адресу) і як до неї достукатися (логін/пароль). * Аналогія: Це головна труба водопостачання, яка входить у ваш будинок. Вона одна на весь застосунок.

2. Session (Сесія) 🛒

Двигун дає з'єднання, але ми не працюємо з ним напряму постійно. Ми створюємо "Сесії". * Аналогія: Уявіть супермаркет. Двигун — це сама будівля магазину. Сесія — це ваш особистий візок. * Ви ходите (робите запити), кладете товари (дані) у візок. * Поки ви не підійдете до каси і не скажете "Сплатити" (команда commit), товари не стануть вашими остаточно. * Важливо: Для кожного запиту користувача (request) ми створюємо нову, чисту сесію, а після завершення — закриваємо її.

3. Base (Основа / Креслення) 📜

Це спеціальний клас, від якого будуть успадковуватися всі наші моделі (таблиці). * Аналогія: Це шаблон документу. Якщо ви хочете створити нову таблицю, ви кажете: "Вона має бути такою, як Base".

Що треба запам'ятати залізно: * Engine створюємо один раз. * Session створюємо для кожного запиту окремо.


3. 🧪 Приклади (від простого до реального)

Ми будемо використовувати сучасний підхід: Asynchronous SQLAlchemy (асинхронність), адже FastAPI створений для швидкості!

Крок 0: Підготовка

Вам знадобляться бібліотеки. У терміналі: pip install sqlalchemy asyncpg (asyncpg — це найшвидший драйвер для PostgreSQL).

Крок 1: Файл database.py

Створимо окремий файл, щоб не засмічувати main.py.

# database.py
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker, declarative_base

# 1. Рядок підключення (Connection String)
# Формат: postgresql+asyncpg://user:password@localhost/dbname
DATABASE_URL = "postgresql+asyncpg://postgres:password@localhost/mydatabase"

# 2. Створюємо Двигун (Engine)
# echo=True змусить SQLAlchemy писати в консоль всі SQL-запити (корисно для навчання!)
engine = create_async_engine(DATABASE_URL, echo=True)

# 3. Фабрика сесій (Session Factory)
# Це "генератор візків" для супермаркету.
SessionLocal = sessionmaker(
    bind=engine,
    class_=AsyncSession,
    expire_on_commit=False
)

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

❓ Що ви очікуєте побачити? Поки що нічого не відбудеться. Ми просто налаштували "трубопровід".

Крок 2: Залежність для FastAPI (Dependency)

Як нам дати кожному запиту користувача його власну сесію?

# database.py (продовження)
from typing import AsyncGenerator

# Це функція-помічник
async def get_db() -> AsyncGenerator:
    async with SessionLocal() as session:
        try:
            yield session  # 1. Даємо сесію (відкриваємо візок)
        finally:
            await session.close() # 2. Забираємо і закриваємо (повертаємо візок на місце)

Пояснення: Ключове слово yield тут працює як пауза. FastAPI візьме сесію, виконає запит, а потім повернеться сюди і виконає блок finally, щоб закрити з'єднання. Це гарантує, що ми не "покладемо" базу тисячами незакритих з'єднань.

Крок 3: Створюємо модель (Таблицю)

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

class User(Base):
    __tablename__ = "users"  # Назва таблиці в SQL

    id = Column(Integer, primary_key=True, index=True)
    username = Column(String, unique=True, index=True)
    email = Column(String)

Дивіться, як це просто! Ми описали Python-клас, а SQLAlchemy буде сприймати це як SQL-таблицю.

Крок 4: Збираємо все в main.py

# main.py
from fastapi import FastAPI, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from database import engine, Base, get_db
from models import User

app = FastAPI()

# УВАГА: У реальному житті ми використовуємо Alembic для міграцій.
# Але для навчання ми попросимо двигун створити таблиці при запуску.
@app.on_event("startup")
async def startup():
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)

@app.post("/users/")
async def create_user(username: str, email: str, db: AsyncSession = Depends(get_db)):
    # 1. Створюємо об'єкт
    new_user = User(username=username, email=email)

    # 2. Додаємо в сесію (кладемо у візок)
    db.add(new_user)

    # 3. Підтверджуємо зміни (йдемо на касу)
    await db.commit()

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

    return new_user

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

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

  1. 🔹 Реплікація: Запустіть наведений вище код. Якщо у вас немає PostgreSQL, змініть DATABASE_URL на sqlite+aiosqlite:///./test.db (не забудьте pip install aiosqlite).
  2. 🔹 Помилка розвідника: Спробуйте змінити пароль у DATABASE_URL на неправильний. Запустіть сервер і спробуйте зробити запит. Прочитайте лог помилки. Що каже сервер? (Це навчить вас розпізнавати проблеми з підключенням).
  3. 🔹 Нова сутність: Створіть у models.py нову модель Product (з полями id, title, price).
  4. 🔹 Читання даних: Напишіть GET-запит /users/, який повертає список користувачів (вам знадобиться result = await db.execute(select(User)) і result.scalars().all()).
  5. 🔹 Міні-кейс: Уявіть, що ви робите блог. Додайте зв'язок: один User може мати багато Post. (Підказка: погугліть SQLAlchemy relationship).

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

Як відрізнити новачка від профі в цій темі?

  1. Безпека (Environment Variables):

    • Новачок: Пише пароль від бази прямо в коді DATABASE_URL = "admin:secret...". Потім заливає це на GitHub, і його базу хакають за 5 хвилин.
    • Профі: Використовує файл .env і бібліотеку python-dotenv. Паролі ніколи не потрапляють у код.
  2. Життєвий цикл сесії:

    • Новачок: Відкриває сесію вручну десь у середині функції і забуває її закрити.
    • Профі: Завжди використовує Depends(get_db). FastAPI сам потурбується про відкриття та закриття. Це надійно.
  3. Асинхронність:

    • Новачок: Блокує головний потік синхронними запитами, поки база думає.
    • Профі: Використовує await, дозволяючи серверу обробляти тисячі інших запитів, поки база даних шукає відповідь.

6. 🧩 Підсумок

Отже, що ми сьогодні зробили? Ми побудували надійний міст між кодом і даними. * Engine — наш тунель до бази. * Session — наш робочий простір. * Models — наші дані у вигляді об'єктів.

Тепер ви можете зберігати і читати дані, не написавши жодного рядка INSERT INTO...!

🚀 Що далі? Зараз наші таблиці створюються автоматично при запуску (Base.metadata.create_all). Але що, як нам треба додати колонку в існуючу таблицю, де вже є живі дані? Видаляти таблицю не можна! На наступному уроці ми познайомимося з Alembic — інструментом для міграцій, який дозволяє змінювати структуру бази даних "на льоту", як хірург.

А поки — кодуйте, помиляйтеся і виправляйте! Це і є шлях. 💻