Модуль 10 · Урок 40 із 58
Psycopg: connections, transactions, repository і tests
Надійний database code визначає не лише SQL, а й межу транзакції: хто відкриває connection, коли зміни стають видимими, що відбувається після помилки і як довести поведінку без production-даних.
Безпечна PostgreSQL-практика модуля 10
Пакет містить synthetic schema/seed/query files, parameterized Python repository, contract tests і read-only verifier. Локальні тести не виконують PostgreSQL; integration run проводиться лише у власній disposable database без персональних чи production-даних, credentials і production endpoints.
Connection — ресурс із явним lifecycle
Application отримує DSN із environment або secret store, а не з repository. Connection і cursor закривають через context managers або try/finally. Pool може бути доречним для server workload, але не потрібен автоматично для короткого CLI; спершу опишіть concurrency і lifetime.
import os
import psycopg
dsn = os.environ["APP_DATABASE_DSN"]
with psycopg.connect(dsn) as connection:
with connection.cursor() as cursor:
cursor.execute("SELECT id, name FROM python_sql_lab.projects ORDER BY id")
rows = cursor.fetchall()DSN не логують, не додають у ZIP і не вставляють у traceback report.
Transaction — business boundary
У Psycopg звичайна операція може почати transaction автоматично. Commit робить changes durable, rollback скасовує їх у поточній transaction. Після SQL error connection лишається в failed transaction state до rollback; продовжувати queries без очищення не можна.
Не тримайте transaction відкритою під час зовнішнього HTTP call або очікування user input: це збільшує contention і ризик stale state.
Autocommit — спеціальний режим
Autocommit не означає «автоматично безпечно». Він доречний для statements, які мають виконуватися поза transaction block, або окремих read/admin scenarios. Для multi-step business change потрібна explicit atomic transaction. Isolation level і read/write mode обирають за concurrency contract, а retry роблять лише для визначених transient failures та idempotent operation.
Retry не повинен дублювати side effects
Якщо transaction повторюється, email, payment або webhook не можна бездумно виконувати всередині кожної спроби. Потрібні idempotency key, outbox або інша узгоджена межа.
Repository відділяє SQL від use case
Repository приймає connection/cursor dependency, виконує parameterized SQL і повертає domain-friendly value. Він не читає глобальні environment variables, не робить commit потай і не перетворює кожну DB exception на None. Transaction owner залишається service/use case, який бачить весь operation.
def close_ticket(cursor, ticket_id: int) -> bool:
cursor.execute(
"UPDATE python_sql_lab.tickets SET status = %s "
"WHERE id = %s AND status = %s",
("closed", ticket_id, "open"),
)
return cursor.rowcount == 1rowcount дає доказ transition: zero може означати missing або вже closed, і цей domain choice треба зафіксувати.
Testing pyramid для database boundary
Pure mapping і validation тестуються без БД. Query contract-тести перевіряють placeholders, identifiers і transaction ownership. Інтеграційні tests мають запускатися на disposable PostgreSQL instance з окремою schema/database, migrations і deterministic seed; вони не повинні торкатися production.
| Layer | Що доводить | Чого не доводить |
|---|---|---|
| Unit | Mapping, branching, domain rules | SQL syntax і PostgreSQL behavior |
| Contract/static | Safe composition та repository boundaries | Реальне execution plan |
| Integration | Migration, constraints, SQL, commit/rollback | Production scale |
| Approved load | Selected workload evidence | Універсальну SLA |
Практичний pack цього модуля проходить deterministic contract tests локально; реальний PostgreSQL integration run учень виконує у власному disposable environment за інструкцією.
Методичні джерела
Урок, сценарії, пояснення й вправи створені SEOWORK. Посилання ведуть лише на офіційну документацію та Python Wiki.
Закріпіть матеріал уроку
Три сценарні питання. Для зарахування уроку потрібно дати щонайменше дві правильні відповіді.