Модуль 33

Асинхронний ORM (async/await)

Це чудова ідея! Стиль Девіда Малана — це золотий стандарт викладання: енергія, чіткість і фокус на розумінні суті.

Ось твій урок про Асинхронний ORM, згенерований за всіма правилами цього майстер-промпту.


🎓 Урок: Асинхронний ORM (async/await)

1. 🔥 Вступ: Чому ваш сервер "завмирає"?

Уявіть, що ви працюєте баристою в найпопулярнішій кав’ярні міста. Ранок, черга аж до дверей.

Приходить клієнт, замовляє складне лате. Ви натискаєте кнопку на кавомашині... і що ви робите далі? 1. Стоїте і дивитеся на цівку кави 40 секунд, ігноруючи решту черги? 2. Чи, поки кава готується, приймаєте замовлення у наступного клієнта?

Якщо ви обрали варіант №1 — ви працюєте синхронно. У світі вебу це катастрофа. Поки один користувач чекає відповідь від бази даних (БД), всі інші бачать білий екран, що "крутиться".

Але якщо ви обрали варіант №2 — вітаю, ви зрозуміли суть асинхронності!

Питання до вас: Чому ми дозволяємо нашим програмам "простоювати", поки база даних шукає інформацію на диску? Адже процесор у цей час вільний!

Сьогодні ми навчимося не просто спілкуватися з базою даних через об’єкти (ORM), а робити це ефективно, не блокуючи роботу всього додатку. Ласкаво просимо у світ Async ORM.


2. 🧠 Теоретична база: Як це працює "під капотом"?

Давайте розберемося з поняттями, щоб не плутатися.

Що таке ORM? (Коротке нагадування)

ORM (Object-Relational Mapping) — це перекладач. Ви говорите мовою об’єктів (Python/JS класів), а ORM перекладає це в SQL-запити для бази даних. * Ви пишете: User.save() * База чує: INSERT INTO users ...

Синхронний vs Асинхронний ORM

  • Синхронний (Sync): Код відправляє запит і засинає. Програма зупиняється на цьому рядку. "Я не зрушу з місця, поки ти не повернеш мені дані про юзера!".
  • Асинхронний (Async): Код відправляє запит і каже: "Окей, база даних, ти поки шукай, а я піду займуся іншими справами. Коли знайдеш — поклич (await), і я заберу результат".

⚙️ Магія Event Loop (Цикл подій)

Уявіть, що всередині вашої програми є менеджер зі списком завдань. Це Event Loop. 1. Коли ви пишете await database.get_users(), ви фактично кажете менеджеру: "Запиши, що ми чекаємо на базу. Я поки вільний". 2. Менеджер дивиться список: "О, тут прийшов новий HTTP-запит, давай поки обробимо його". 3. Коли база даних відповідає, менеджер каже: "Гей, дані прийшли! Повертаємося до виконання попередньої функції".

❗️ Запам’ятати обов’язково: Ключові слова async та await — це ваші інструменти управління часом. * async def — оголошує функцію, яка вміє "призупинятися". * await — точка, де ми кажемо: "Тут треба почекати, але не блокуй усіх інших".


3. 🧪 Приклади (на базі Python + SQLAlchemy)

Для прикладів використаємо Python, бо це класика бекенду, але логіка ідентична в Node.js (Prisma/TypeORM).

Приклад 1: Мінімум для старту

Що ви очікуєте побачити при підключенні? Напевно, просто рядок з URL бази. Але в асинхронному світі нам потрібен спеціальний драйвер (наприклад, asyncpg для PostgreSQL), бо звичайний драйвер заблокує все.

from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker

# Зверніть увагу: postgresql+asyncpg (асинхронний драйвер!)
DATABASE_URL = "postgresql+asyncpg://user:pass@localhost/dbname"

# Створення двигуна
engine = create_async_engine(DATABASE_URL, echo=True)

# Фабрика сесій (наш "портал" у базу)
async_session = sessionmaker(
    engine, class_=AsyncSession, expire_on_commit=False
)

Пояснення: Ми створили двигун, який вміє "не блокувати". echo=True покаже нам SQL, який генерується.


Приклад 2: Типовий запит (SELECT)

У звичайному коді ми б написали users = session.query(User).all(). Як ви думаєте, як це виглядатиме в async?

from sqlalchemy import select

async def get_all_users():
    async with async_session() as session:
        # 1. Створюємо запит (це ще не виклик до БД!)
        stmt = select(User).where(User.age > 18)

        # 2. Виконуємо запит АСИНХРОННО
        print("Відправляємо запит...")
        result = await session.execute(stmt) 
        print("Отримали відповідь!")

        # 3. Витягуємо дані
        users = result.scalars().all()

        for user in users:
            print(f"Знайдено: {user.name}")

# Запуск (у реальному додатку це робить фреймворк, тут — ми самі)
import asyncio
asyncio.run(get_all_users())

Чому так? Ключовий момент — await session.execute(stmt). Саме в цю мілісекунду управління передається іншим частинам програми. Якби тут не було await, ми б отримали об'єкт "очікування" (coroutine), а не дані.


Приклад 3: Вищий пілотаж (Конкурентність)

Уявіть, треба знайти інформацію про трьох різних користувачів. Синхронний підхід: Запит 1 (чекаємо), потім Запит 2 (чекаємо), потім Запит 3 (чекаємо). Сума часу: 3 секунди. Асинхронний підхід: Запускаємо всі три запити "паралельно" і чекаємо, коли завершаться всі. Час: ~1 секунда (найдовшого запиту).

async def get_user_by_id(user_id):
    async with async_session() as session:
        result = await session.execute(select(User).where(User.id == user_id))
        return result.scalars().first()

async def main():
    # Запускаємо три задачі одночасно!
    task1 = get_user_by_id(1)
    task2 = get_user_by_id(2)
    task3 = get_user_by_id(3)

    # await asyncio.gather чекає виконання їх усіх
    print("Шукаємо всіх трьох одразу...")
    user1, user2, user3 = await asyncio.gather(task1, task2, task3)

    print(f"Знайшли: {user1.name}, {user2.name}, {user3.name}")

Результат: Це працює набагато швидше, ніж послідовне виконання.


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

Час забруднити руки кодом!

🔹 Завдання 1: "Hello Async World" Налаштуйте з'єднання з базою (можна використати SQLite з драйвером aiosqlite для простоти: sqlite+aiosqlite:///test.db). Створіть просту модель Item (id, name, price) і напишіть асинхронну функцію, яка додає один товар у базу.

🔹 Завдання 2: "Знайди помилку" Студент написав код:

users = session.execute(select(User))
print(users.scalars().all())

Код видає помилку або дивний об'єкт. Чому? Виправте код. (Підказка: де ключове слово на літеру 'a'?)

🔹 Завдання 3: "Масова знижка" Напишіть функцію apply_discount(), яка вибирає всі товари дорожче 100 грн і зменшує їх ціну на 10%. Важливо: зробіть commit змін у базу асинхронно.

🔹 Завдання 4: Міні-кейс "Дашборд адміністратора" Вам треба відобразити на екрані: 1. Кількість користувачів. 2. Кількість замовлень за сьогодні. 3. Суму продажів. Напишіть код, який збирає ці три метрики одночасно (використовуючи gather), а не послідовно.

🔹 Питання "А що, якщо..." А що буде, якщо всередині вашої асинхронної функції викличете time.sleep(5) замість await asyncio.sleep(5)? Як це вплине на користувачів вашого сайту?


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

Ви тепер знаєте синтаксис, але як думає профі?

⚠️ Типові пастки новачків:

  1. Забутий await: Ви викликаєте функцію, а вона повертає об'єкт coroutine замість даних. Це класика.
  2. Синхронщина в асинхронності: Якщо ви вставите "важкий" цикл обробки даних або синхронний запит до API всередині async функції — ви заблокуєте Event Loop. Весь сервер "стане колом".
  3. Lazy Loading (Ліниве завантаження): В синхронному ORM ви могли написати user.posts, і ORM сама робила запит. В Async ORM це часто не працює або викликає помилку, бо не можна зробити запит "магічно" без await.
    • Рішення: Використовуйте "eager loading" (завантажуйте зв'язані дані одразу в головному запиті через options(selectinload(User.posts))).

🎓 Порада від сеньйора:

Асинхронність — це не про те, щоб зробити один запит швидшим. Це про те, щоб ваш сервер міг тримати тисячу запитів одночасно, не купуючи при цьому суперкомп'ютер. Думайте про "пропускну здатність" (throughput), а не просто про "швидкість" (latency).


6. 🧩 Підсумок

Отже, що ми сьогодні зрозуміли? 1. Синхронний код блокує чергу, асинхронний дозволяє обслуговувати інших, поки чекаємо. 2. async/await — це спосіб сказати: "Я почекаю результат тут, а процесор хай займеться іншим". 3. В ORM ми повинні явно писати await для будь-якої операції, яка йде в мережу або на диск.

Тепер ви вмієте: ✅ Підключатися до БД асинхронно. ✅ Робити SELECT та INSERT без блокування. ✅ Виконувати кілька запитів паралельно для прискорення.

🔍 Що далі? У нас є дані, ми вміємо їх швидко діставати. Але як віддати їх світу? На наступному уроці ми поєднаємо це з FastAPI і створимо повноцінний, блискавичний API, який витримає навантаження Reddit!

Готові? Тоді до зустрічі! 👋