Модуль 24

Асинхронний SQLAlchemy

Ось готовий урок, створений за твоїм майстер-промптом.


🎓 CS50-Style Lesson: Асинхронний SQLAlchemy

Привіт, друзі! 👋 Це CS50 (умовно кажучи), і сьогодні ми заглянемо під капот сучасної веб-розробки.

1. 🔥 Вступ: Коли одна черга вбиває весь бізнес

Уявіть, що ви відкрили найпопулярнішу кав'ярню в місті. У вас чудовий бариста (це ваш CPU або процесор), але є проблема з касою.

У синхронному світі (це класичний підхід) касир приймає замовлення, кричить баристі «Одне лате!», і... завмирає. Він стоїть і дивиться на кавомашину. Він не приймає нових клієнтів. Він не видає чеки. Він просто чекає, поки кава наллється в чашку. Черга росте, клієнти лютують, бізнес горить.

Абсурд? Так. Але саме так працює звичайний код при роботі з базою даних (БД). Ваш сервер відправляє запит у БД і "засинає", чекаючи відповіді.

А тепер асинхронний світ. Касир приймає замовлення, віддає тікет баристі й одразу каже наступному клієнту: «Добрий день, що бажаєте?». Поки кава готується, касир встиг обслужити ще 10 людей.

Риторичне запитання: Якщо ви будуєте швидкий API (наприклад, на FastAPI), навіщо вам швидкий Python, якщо він 90% часу просто чекає на відповідь від бази даних?

Сьогодні ми навчимося не чекати. Ми розберемо Асинхронний SQLAlchemy.


2. 🧠 Теоретична база: Як не блокувати світ

Щоб зрозуміти асинхронність у SQLAlchemy (версії 1.4 і особливо 2.0+), треба засвоїти кілька концепцій.

1. Драйвери (Drivers)

Звичайний psycopg2 (для PostgreSQL) або стандартний модуль sqlite3 — синхронні. Якщо ви спробуєте використати їх з async/await, магії не станеться. Нам потрібні спеціальні драйвери, які вміють "спілкуватися" з Event Loop (циклом подій). * Для PostgreSQL: asyncpg * Для SQLite: aiosqlite

2. Engine vs AsyncEngine

Раніше ми створювали Engine — це серце нашого підключення. Тепер ми створюємо AsyncEngine. Це та сама "труба" до бази, але вона не блокує потік, поки по ній течуть дані.

3. Session vs AsyncSession

Сесія — це ваша "транзакція", ваш робочий простір. * Синхронно: Ви кажете session.execute(...), і код стоїть. * Асинхронно: Ви кажете await session.execute(...). Слово await — це маркер. Воно каже Python: "Поки база даних шукає ці дані, іди роби щось корисне (наприклад, оброби інший запит користувача)".

🧐 Що треба запам'ятати залізно:

У SQLAlchemy 2.0 ми переходимо на новий синтаксис запитів. Ми більше не пишемо session.query(User). Ми пишемо result = await session.execute(select(User)). Це універсальний стиль, який працює і для синхронного, і для асинхронного коду.


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

Для прикладів використаємо SQLite, бо це просто, але пам'ятайте: у продакшн ми візьмемо PostgreSQL.

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

Нам потрібні бібліотеки:

pip install sqlalchemy aiosqlite

Приклад 1: Налаштування двигуна (Engine)

Як це виглядало б у звичайному коді? create_engine("sqlite:///db.sqlite3"). А ось як це виглядає в асинхронному світі:

import asyncio
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import select

# Зверніть увагу на "+aiosqlite" у рядку підключення!
DATABASE_URL = "sqlite+aiosqlite:///async_db.sqlite3"

# Створюємо асинхронний рушій
engine = create_async_engine(DATABASE_URL, echo=True)

# Фабрика сесій (щоб щоразу не створювати руками)
async_session = async_sessionmaker(engine, expire_on_commit=False)

# Базовий клас для моделей (стандарт SQLAlchemy 2.0)
class Base(DeclarativeBase):
    pass

# Наша модель
class Student(Base):
    __tablename__ = "students"

    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    house: Mapped[str] # Gryffindor, Slytherin etc.

    def __repr__(self):
        return f"<Student {self.name} from {self.house}>"

Приклад 2: Створення таблиць (Трохи магії)

Ви очікуєте простого Base.metadata.create_all(engine). Але create_all — синхронна функція! А наш engine — асинхронний. Що робити? Ми використовуємо метод run_sync.

async def init_db():
    async with engine.begin() as conn:
        # Ми просимо двигун виконати синхронну функцію в асинхронному контексті
        await conn.run_sync(Base.metadata.create_all)
    print("✅ Таблиці створено!")

Приклад 3: Додавання та читання (CRUD)

Запитання до вас: де ми маємо поставити await? Правильно, скрізь, де йде звернення до мережі або диску (до БД).

async def main():
    await init_db() # Спершу створимо таблиці

    # 1. Додавання даних
    async with async_session() as session:
        harry = Student(name="Harry Potter", house="Gryffindor")
        draco = Student(name="Draco Malfoy", house="Slytherin")

        session.add_all([harry, draco])

        # Момент істини: запис у файл/БД
        await session.commit() 
        print("✅ Студентів додано.")

    # 2. Читання даних
    async with async_session() as session:
        # Старий стиль session.query() тут не працює!
        # Ми формуємо SQL-запит об'єктом select()
        query = select(Student).where(Student.house == "Gryffindor")

        # Виконуємо запит
        result = await session.execute(query)

        # Витягуємо саме об'єкти (scalars)
        students = result.scalars().all()

        print("\n🔍 Знайдено ґрифіндорців:")
        for s in students:
            print(f" - {s.name}")

# Запуск асинхронної програми
if __name__ == "__main__":
    asyncio.run(main())

Чому результат такий? Коли ми викликаємо await session.execute(query), Python відпускає керування. Якби в цей момент прийшов HTTP-запит, сервер міг би його обробити. Як тільки SQLite відповів, код продовжив виконання з рядка result = ....


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

Час забруднити руки кодом. Відкрийте IDE і спробуйте наступне:

🔹 Завдання 1: Setup Скопіюйте код вище, запустіть і переконайтеся, що файл async_db.sqlite3 створився.

🔹 Завдання 2: Оновлення (Update) Напишіть функцію update_house(student_name, new_house). * Крок 1: Зробіть select студента за іменем. * Крок 2: Змініть поле .house. * Крок 3: Зробіть await session.commit(). * Підказка: Не забудьте scalars().first() або scalar_one_or_none().

🔹 Завдання 3: Видалення (Delete) Створіть функцію, яка видаляє всіх студентів зі Слизерину (вибач, Драко).

🔹 Завдання 4: Міні-кейс "Сортувальний капелюх" Створіть список із 5 нових імен. Напишіть цикл, який асинхронно додає їх у базу, призначаючи факультет випадковим чином (random.choice). Питання: Чи варто робити commit() після кожного студента, чи краще один раз в кінці? Чому?

🔹 Питання "А що, якщо..." Що станеться, якщо всередині async with session: ви напишете time.sleep(5) замість await asyncio.sleep(5)? (Спойлер: Ви вб'єте всю асинхронність. Спробуйте і подивіться, як програма зависне).


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

Як досвідчений інженер дивиться на Async SQLAlchemy?

  1. Проблема N+1 (Lazy Loading). У синхронному коді ви могли написати student.grades і SQLAlchemy сама б "збігала" в базу за оцінками. В асинхронному коді це заборонено. Чому? Бо доступ до атрибута не може бути await.

    • Як думає про: "Якщо мені потрібні пов'язані дані (наприклад, оцінки студента), я маю завантажити їх одразу в запиті, використовуючи options(selectinload(Student.grades))."
  2. Context Managers — це життя. Завжди використовуйте async with. Якщо ви відкриєте сесію і забудете її закрити (або станеться помилка без try/finally), з'єднання з базою зависне. А пул з'єднань не гумовий.

  3. Не блокуйте Event Loop. Найбільша помилка новачків — використовувати синхронні бібліотеки всередині асинхронних функцій. Якщо ви парсите дані для запису в БД, переконайтеся, що бібліотека парсингу не блокує процес.


6. 🧩 Підсумок

Отже, що ми сьогодні зробили? 1. Зрозуміли, що чекати — це дорого. 2. Налаштували асинхронний драйвер (aiosqlite). 3. Навчилися робити CRUD операції, не блокуючи виконання програми. 4. Перейшли на сучасний стиль запитів SQLAlchemy 2.0 (select, scalars).

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

👀 Тизер: Але що, як структура вашої бази зміниться? Що, як треба додати колонку "Вік"? Видаляти файл бази і створювати заново? Ні! На наступному уроці ми познайомимося з Alembic — інструментом для міграцій, який дозволяє змінювати базу даних на льоту, як хірурги роблять операцію на працюючому серці.

Це був CS50. (Drop mic 🎤)