Ось готовий урок, створений за твоїм майстер-промптом.
🎓 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?
-
Проблема N+1 (Lazy Loading). У синхронному коді ви могли написати
student.gradesі SQLAlchemy сама б "збігала" в базу за оцінками. В асинхронному коді це заборонено. Чому? Бо доступ до атрибута не може бутиawait.- Як думає про: "Якщо мені потрібні пов'язані дані (наприклад, оцінки студента), я маю завантажити їх одразу в запиті, використовуючи
options(selectinload(Student.grades))."
- Як думає про: "Якщо мені потрібні пов'язані дані (наприклад, оцінки студента), я маю завантажити їх одразу в запиті, використовуючи
-
Context Managers — це життя. Завжди використовуйте
async with. Якщо ви відкриєте сесію і забудете її закрити (або станеться помилка без try/finally), з'єднання з базою зависне. А пул з'єднань не гумовий. -
Не блокуйте Event Loop. Найбільша помилка новачків — використовувати синхронні бібліотеки всередині асинхронних функцій. Якщо ви парсите дані для запису в БД, переконайтеся, що бібліотека парсингу не блокує процес.
6. 🧩 Підсумок
Отже, що ми сьогодні зробили?
1. Зрозуміли, що чекати — це дорого.
2. Налаштували асинхронний драйвер (aiosqlite).
3. Навчилися робити CRUD операції, не блокуючи виконання програми.
4. Перейшли на сучасний стиль запитів SQLAlchemy 2.0 (select, scalars).
Тепер ви вмієте: Створювати бекенд, який може обробляти тисячі запитів, не захлинаючись на очікуванні відповіді від бази даних.
👀 Тизер: Але що, як структура вашої бази зміниться? Що, як треба додати колонку "Вік"? Видаляти файл бази і створювати заново? Ні! На наступному уроці ми познайомимося з Alembic — інструментом для міграцій, який дозволяє змінювати базу даних на льоту, як хірурги роблять операцію на працюючому серці.
Це був CS50. (Drop mic 🎤)