Django ORM та БД — Mermaid-схеми¶
Всі діаграми — візуалізація концепцій з усіх файлів документації. Використовуй після читання теорії щоб закріпити розуміння через схеми. Порядок: ER → Зв'язки → ORM Flow → Query Lifecycle → Transactions → Architecture
🔬 Хочеш побачити ці схеми у дії?
docs/reference/legacy_source/orm_laboratory_reference.md— кожна mermaid-діаграма має відповідний code cell де виконується реальний SQL.
Mermaid-схема в цьому файлі Розділ ноутбука ER-діаграма / схема таблиць ## 0. 🧱 Що таке object в Django ORM?ORM → SQL Lifecycle ## 1. 🧠 ORM Mental ModelLazy Evaluation flow ## 1. 🧠 ORM Mental ModelN+1 vs select_related ## 8. ⚡ N+1 Problem & Optimizationselect_related vs prefetch_related ## 8. ⚡ N+1 Problem & Optimizationtransaction.atomic() SQL lifecycle ## 13. 🔒 ТранзакціїServices/Selectors архітектура README.md→ секція03 · APPLICATION LAYERS
1. ER-діаграма — Notes Platform (повний проект)¶
Що показує: Всі таблиці проекту
notes_chat_appз усіма зв'язками.||--o|= 1:1,||--o{= 1:N,}o--o{= M:N. FK завжди на стороні "багатьох" (Many side).
erDiagram
USER ||--o| USER_PROFILE : "1:1 OneToOneField"
USER ||--o{ NOTEBOOK : "1:N ForeignKey"
USER ||--o{ NOTE : "1:N ForeignKey"
USER ||--o{ TODO_LIST : "1:N ForeignKey"
USER ||--o{ SHOPPING_LIST : "1:N ForeignKey"
USER ||--o{ TAG : "1:N ForeignKey"
NOTEBOOK ||--o{ NOTE : "1:N SET_NULL"
NOTE }o--o{ TAG : "M:N junction table"
NOTE ||--o{ REMINDER : "1:N CASCADE"
TODO_LIST ||--o{ TODO_ITEM : "1:N CASCADE"
SHOPPING_LIST ||--o{ SHOP_ITEM : "1:N CASCADE"
USER {
bigint id PK
string username
string email
string password_hash
}
USER_PROFILE {
bigint id PK
bigint user_id FK "UNIQUE → 1:1"
string display_name
string timezone
}
NOTEBOOK {
bigint id PK
bigint user_id FK
string title
string color
boolean is_default
}
NOTE {
bigint id PK
bigint user_id FK
bigint notebook_id FK "nullable"
string title
text content
int priority
boolean is_pinned
boolean is_archived
timestamp updated_at
}
TAG {
bigint id PK
bigint user_id FK
string name
string color
}
NOTE_TAG_JUNCTION {
bigint note_id FK
bigint tag_id FK
}
REMINDER {
bigint id PK
bigint note_id FK
timestamp remind_at
boolean is_sent
string repeat_pattern
}
TODO_LIST {
bigint id PK
bigint user_id FK
string title
boolean is_completed
}
TODO_ITEM {
bigint id PK
bigint todo_list_id FK
string text
boolean is_done
int order_position
date due_date
}
SHOPPING_LIST {
bigint id PK
bigint user_id FK
string title
string store_name
}
SHOP_ITEM {
bigint id PK
bigint shopping_list_id FK
string name
decimal quantity
string unit
boolean is_purchased
decimal estimated_price
}
2. Де живе FK — правило "Many side"¶
Що показує: Наочно де записується Foreign Key при 1:N та M:N зв'язках. FK завжди в таблиці "Many" (не в "One"!).
graph LR
subgraph "1:1 — OneToOneField"
U1[User id=5] <-->|user_id FK + UNIQUE| UP1[UserProfile user_id=5]
N1["FK в UserProfile\n(FK + UNIQUE = 1:1)"]
end
subgraph "1:N — ForeignKey"
NB[Notebook id=1] -->|"contains"| NO1[Note notebook_id=1]
NB --> NO2[Note notebook_id=1]
NB --> NO3[Note notebook_id=1]
N2["FK в Note\n(сторона Many)"]
end
subgraph "M:N — ManyToManyField"
N[Note id=1] -->|through| JT[note_tags\nnote_id=1, tag_id=10\nnote_id=1, tag_id=11]
JT -->|"tagged"| T1[Tag id=10]
JT --> T2[Tag id=11]
N3["Junction table\n(автоматично Django)"]
end
3. ORM → SQL — Lifecycle запиту¶
Що показує: Повний шлях від Python коду до SQL і назад. Ключовий момент: QuerySet ЛІНИВИЙ — SQL виконується тільки при evaluation.
graph TD
A["Python QuerySet
Note.objects.filter(user=alice)
select_related(notebook)
prefetch_related(tags)
order_by(updated_at DESC)"]
B["QuerySet object created"]
C{"Evaluation triggered?"}
D["SQLCompiler.as_sql()"]
E["SQL #1
SELECT notes + notebooks
JOIN notebook table
WHERE user_id=5"]
F["SQL #2
Prefetch tags
WHERE note_id IN (...)"]
G["psycopg2
TCP connection
PostgreSQL"]
H["ModelIterable
Create Note objects"]
I["Related objects loaded
notebook cached
tags prefetched"]
A --> B
B --> C
C -->|"No"| B
C -->|"Yes"| D
D --> E
E --> F
F --> G
G --> H
H --> I
style A fill:#0C4B33,color:#fff
style G fill:#336791,color:#fff
style C fill:#8B5E3C,color:#fff
4. Lazy Evaluation — коли виконується SQL¶
Що показує: QuerySet методи що НЕ виконують SQL (повертають QuerySet) vs методи що ВИКОНУЮТЬ SQL (evaluation triggers).
graph LR
subgraph "НЕ виконують SQL (lazy)"
L1[".filter()"] --> QS[QuerySet]
L2[".exclude()"] --> QS
L3[".order_by()"] --> QS
L4[".select_related()"] --> QS
L5[".prefetch_related()"] --> QS
L6[".annotate()"] --> QS
L7[".values()"] --> QS
QS -.->|"можна ланцюжити скільки завгодно"| QS
end
subgraph "ВИКОНУЮТЬ SQL (evaluation)"
QS --> E1["list(qs)"]
QS --> E2["for note in qs"]
QS --> E3["qs[0], qs[:10]"]
QS --> E4["qs.first(), qs.last()"]
QS --> E5["qs.count()"]
QS --> E6["len(qs)"]
QS --> E7["bool(qs)"]
end
style E1 fill:#8B0000,color:#fff
style E2 fill:#8B0000,color:#fff
style E3 fill:#8B0000,color:#fff
style E4 fill:#8B0000,color:#fff
style E5 fill:#8B0000,color:#fff
5. N+1 Problem vs select_related¶
Що показує: Різниця в кількості SQL запитів між N+1 та select_related. Без оптимізації: 1 + N запитів. З select_related: 1 запит.
sequenceDiagram
participant App as Django View
participant DB as PostgreSQL
Note over App,DB: ❌ N+1 PROBLEM (100 нотаток)
App->>DB: SELECT * FROM notes WHERE status='published' LIMIT 100
DB-->>App: 100 rows (user_id=5,3,7,8,...)
App->>DB: SELECT * FROM auth_user WHERE id=5
App->>DB: SELECT * FROM auth_user WHERE id=3
App->>DB: SELECT * FROM auth_user WHERE id=7
Note over App,DB: ... ще 97 запитів ...
Note over App,DB: ВСЬОГО: 101 SQL запит!
Note over App,DB: ✅ select_related (ЗАВЖДИ 1 запит)
App->>DB: SELECT n.*, u.*, nb.* FROM notes n LEFT JOIN users u ON n.user_id=u.id LEFT JOIN notebooks nb ON n.notebook_id=nb.id WHERE status='published' LIMIT 100
DB-->>App: 100 rows (вже з user + notebook даними!)
Note over App,DB: note.user.username → з кешу 0 SQL!
6. select_related vs prefetch_related — архітектура¶
Що показує: Механізм кожного методу на рівні SQL. select_related = SQL JOIN. prefetch_related = окремий запит + Python join.
graph TD
subgraph "select_related → SQL JOIN"
A1["Note.objects.select_related('user', 'notebook')"] --> B1[1 SQL з JOIN]
B1 --> C1["SELECT n.*, u.*, nb.*\nFROM notes n\nLEFT JOIN users u ON n.user_id=u.id\nLEFT JOIN notebooks nb ON n.notebook_id=nb.id"]
C1 --> D1["Всі дані в одному рядку результату"]
D1 --> E1["✅ Для FK та OneToOne"]
end
subgraph "prefetch_related → Python JOIN"
A2["Note.objects.prefetch_related('tags')"] --> B2[SQL #1: Fetch Notes]
B2 --> C2["SELECT * FROM notes"]
A2 --> B3[SQL #2: Fetch Tags]
B3 --> C3["SELECT t.*, nt.note_id\nFROM tags t INNER JOIN note_tags nt\nWHERE nt.note_id IN (1,2,3,...,100)"]
C2 --> D2["Python: obj._prefetched_objects_cache\nзаповнюється dict note_id → [tags]"]
C3 --> D2
D2 --> E2["✅ Для ManyToMany та reverse FK"]
end
7. Migration Lifecycle¶
Що показує: Повний цикл міграції від зміни моделі до оновленої БД. Ключовий факт:
makemigrationsне змінює БД,migrate— змінює.
sequenceDiagram
participant Dev as Developer
participant Django as Django ORM
participant FS as Filesystem
participant DB as PostgreSQL
Dev->>Django: Змінити models.py
Dev->>Django: manage.py makemigrations
Django->>Django: Порівняти models.py з migration graph
Django->>FS: Створити 0003_add_field.py
FS-->>Dev: Файл готовий
Dev->>Dev: Перевірити файл міграції!
Dev->>Django: manage.py sqlmigrate app 0003
Django-->>Dev: Показати SQL без виконання
Dev->>Django: manage.py migrate
Django->>DB: ALTER TABLE ADD COLUMN ...
DB-->>Django: OK
Django->>DB: INSERT INTO django_migrations (app, name, applied)
Django-->>Dev: Applied 0003_add_field
8. Migration Dependency Graph¶
Що показує: Як міграції залежать одна від одної. Django виконує в правильному порядку залежностей (DAG).
graph TD
A["notes_app 0001_initial\nCREATE TABLE notes"] --> B["notes_app 0002_extended\nCREATE TABLE notebooks, tags..."]
A --> C["notes_app 0002_extended\nalso depends on:"]
D["auth 0001_initial\nCREATE TABLE auth_user"] --> B
B --> E["notes_app 0003_add_index\nCREATE INDEX"]
B --> F["notes_app 0004_add_word_count\nADD COLUMN word_count"]
E --> G["notes_app 0005_add_constraint\nADD CONSTRAINT CHECK"]
F --> G
style A fill:#2C7A2C,color:#fff
style D fill:#2C7A2C,color:#fff
style G fill:#336791,color:#fff
9. Query Execution Lifecycle — 8 фаз¶
Що показує: Що відбувається в PostgreSQL з SQL запитом від парсингу до результату.
graph TD
A["SQL: SELECT * FROM notes\nWHERE user_id=5 ORDER BY updated_at DESC"] --> B
B["1. PARSING\nLexer + Parser\nTokenize SQL\nBuild AST"] --> C
C["2. SEMANTIC ANALYSIS\nПеревірити таблиці\nПеревірити права\nSystem catalog lookup"] --> D
D["3. REWRITER\nView expansion\nRule system application"] --> E
E["4. PLANNER: Enumerate plans\nPlan A: Seq Scan\nPlan B: Index Scan user_id\nPlan C: Index Scan updated_at"] --> F
F["5. OPTIMIZER: Choose cheapest\ncost(Seq Scan)=5000 ← відхилено\ncost(Index Scan)=23 ← обрано!\n(based on table statistics)"] --> G
G["6. EXECUTION PLAN\nIndex Scan on note_user_updated_idx\n→ Heap fetch"] --> H
H["7. EXECUTION ENGINE\nRead index pages\nFetch heap tuples\nApply remaining filters"] --> I
I["8. RESULT\nRows → psycopg2 cursor\n→ Django ModelIterable\n→ List[Note]"]
style A fill:#4A90D9,color:#fff
style F fill:#0C4B33,color:#fff
style I fill:#336791,color:#fff
10. B-Tree Index — навігація пошуку¶
Що показує: Як B-Tree індекс знаходить запис за O(log N) замість O(N). Кожен рівень = один disk I/O. Глибина: log₂(N) рівнів.
graph TD
Root["Root Node\n[250, 500, 750]"] --> B1["Branch\n[10, 50, 100]"]
Root --> B2["Branch\n[260, 300, 400]"]
Root --> B3["Branch\n[510, 600, 700]"]
Root --> B4["Branch\n[760, 800, 900]"]
B1 --> L1["Leaf [1,2,3,4,5]\nTID→heap"]
B1 --> L2["Leaf [6,7,8,9]\nTID→heap"]
B1 --> L3["Leaf [11,12,15]\nTID→heap"]
L2 -.->|"двозв'язний список\n(для range scans)"| L3
style Root fill:#8B5E3C,color:#fff
style L2 fill:#2C7A2C,color:#fff
Пошук user_id=7: Root:
7 < 250→ Branch[10,50,100] →7 < 10→ Leaf[6,7,8,9] → знайдено TID → heap fetch. 3-4 disk I/O для мільярда рядків.
11. transaction.atomic() — SQL lifecycle¶
Що показує: Що відбувається в SQL при transaction.atomic() та при exception.
sequenceDiagram
participant PY as Python (atomic block)
participant PG as PostgreSQL
PY->>PG: BEGIN (при вході в atomic())
PY->>PG: INSERT INTO notes (title, ...) VALUES ('Django', ...)
PG-->>PY: id=42
PY->>PG: SAVEPOINT sp_1 (вкладений atomic)
PY->>PG: INSERT INTO reminders (...) VALUES (...)
PG-->>PY: id=5
alt Успіх (без exception):
PY->>PG: RELEASE SAVEPOINT sp_1
PY->>PG: COMMIT
PG-->>PY: OK
Note over PY,PG: on_commit() callbacks виконуються тут
else Exception у вкладеному atomic:
PY->>PG: ROLLBACK TO SAVEPOINT sp_1
Note over PY,PG: тільки reminder відкатився, note збережена
PY->>PG: COMMIT (зовнішній успішно)
else Exception у зовнішньому atomic:
PY->>PG: ROLLBACK
Note over PY,PG: ВСЕ відкатилось, on_commit НЕ виконується
end
12. select_for_update() — Row Locking Timeline¶
Що показує: Як select_for_update() запобігає race condition при конкурентному доступі.
sequenceDiagram
participant T1 as Transaction 1 (Alice)
participant T2 as Transaction 2 (Bob)
participant PG as PostgreSQL
T1->>PG: BEGIN
T1->>PG: SELECT * FROM events WHERE id=1 FOR UPDATE
PG-->>T1: event(seats=1) ← lock набутий!
T2->>PG: BEGIN
T2->>PG: SELECT * FROM events WHERE id=1 FOR UPDATE
Note over T2,PG: T2 ЧЕКАЄ (рядок заблокований T1)
T1->>PG: UPDATE events SET seats=0 WHERE id=1
T1->>PG: INSERT INTO bookings (event_id, user_id) VALUES (1, alice)
T1->>PG: COMMIT ← lock знімається
PG-->>T2: event(seats=0) ← T2 отримує lock, бачить seats=0
T2->>T2: seats=0 → raise ValueError("Немає місць")
T2->>PG: ROLLBACK
13. Deadlock — взаємне блокування¶
Що показує: Як виникає deadlock при різному порядку блокування. Рішення: завжди блокувати в стабільному порядку (по id ASC).
graph LR
subgraph "Deadlock ситуація"
T1A["T1: lock(note_id=1)"] -->|"чекає"| T1B["T1: lock(note_id=2)"]
T2B["T2: lock(note_id=2)"] -->|"чекає"| T2A["T2: lock(note_id=1)"]
T1A -.->|"заблокований T2"| T2B
T2B -.->|"заблокований T1"| T1A
end
subgraph "Рішення: стабільний порядок"
S1["T1 і T2: завжди lock\norder_by('id') ASC"] --> S2["T1: lock(1) → lock(2)\nT2: lock(1) ← чекає T1\n(НЕ бере lock(2) першим!)"]
S2 --> S3["T1 завершує → T2 продовжує\nDeadlock неможливий!"]
end
14. MVCC — Tuple Versioning¶
Що показує: Як PostgreSQL зберігає кілька версій рядка для MVCC. xmin = транзакція що створила, xmax = транзакція що видалила/замінила.
graph TD
subgraph "Таблиця notes (внутрішньо)"
T1["Tuple v1\nid=42, title='Django'\nxmin=100, xmax=NULL\nВидимий: txid >= 100"]
T2["Tuple v2\nid=42, title='Django ORM'\nxmin=200, xmax=NULL\nВидимий: txid >= 200"]
T3["Tuple v1 (мертвий)\nid=42, title='Django'\nxmin=100, xmax=200\nВидимий: 100 <= txid < 200"]
end
TX150["Транзакція txid=150\n(почалась до UPDATE)"] -->|"бачить"| T3
TX250["Транзакція txid=250\n(почалась після UPDATE)"] -->|"бачить"| T2
VACUUM["VACUUM\n(прибирає мертві tuple)"] -->|"видаляє"| T3
15. N+1 vs оптимізовані запити — повна картина¶
Що показує: Кількість SQL запитів для різних стратегій завантаження 100 нотаток.
graph LR
subgraph "❌ N+1 (101 запит)"
Q1["SELECT * FROM notes LIMIT 100"] --> R1["100 рядків"]
R1 -->|"for note in notes:"| Q2["SELECT * FROM users WHERE id=X\n× 100 разів"]
end
subgraph "✅ select_related (1 запит)"
Q3["SELECT n.*, u.*, nb.*\nFROM notes JOIN users JOIN notebooks\nLIMIT 100"] --> R3["100 рядків (з user+notebook)"]
end
subgraph "✅ prefetch_related (2 запити)"
Q4["SELECT * FROM notes LIMIT 100"] --> R4["100 рядків"]
Q5["SELECT t.*, nt.note_id FROM tags\nINNER JOIN note_tags\nWHERE note_id IN (1..100)"] --> R5["теги в кеші"]
R4 --> R6["Python join у пам'яті"]
R5 --> R6
end
16. PostgreSQL System Architecture¶
Що показує: Як PostgreSQL обробляє Django запит на системному рівні. Shared Buffers = кеш. WAL = журнал відмовостійкості.
graph TD
Client["Django + psycopg2\nTCP connection"] --> PM["Postmaster :5432\nприймає з'єднання"]
PM -->|"fork()"| BP["Backend Process\n(ізольований процес\nдля кожного з'єднання)"]
BP --> QP["Parser → AST\n→ Planner → Optimizer\n→ Execution Plan"]
QP --> EE["Execution Engine"]
EE <-->|"read/write"| SB["Shared Buffers\n(25% RAM кеш\nspільний для всіх процесів)"]
SB -->|"dirty pages"| WW["WAL Writer\n(WRITE-AHEAD LOG)"]
WW -->|"fsync"| WAL["WAL Files\n/pg_wal/\n(журнал спочатку!)"]
WAL -.->|"checkpoint\n(async)"| DF["Data Files\n/base/\n(actual tables)"]
style PM fill:#336791,color:#fff
style SB fill:#0C4B33,color:#fff
style WAL fill:#8B5E3C,color:#fff
17. Services/Selectors архітектура¶
Що показує: Потік даних через шари Services/Selectors архітектури. View — тонкий. Логіка — в services/selectors.
graph TD
REQ["HTTP Request\n(GET /notes/?q=python)"] --> VIEW["views.note_list()\nПарсить GET параметри"]
VIEW -->|"get_user_notes(user, search='python')"| SEL["selectors.py\nBUILDS QuerySet\nselect_related + prefetch_related"]
SEL -->|"SQL"| DB[(PostgreSQL)]
DB -->|"rows"| SEL
SEL -->|"QuerySet[Note]"| VIEW
VIEW -->|"render(template, context)"| TMPL["note_list.html\nітерує notes\n0 нових SQL!"]
REQ2["HTTP Request\n(POST /notes/create/)"] --> VIEW2["views.note_create()\nПарсить form.cleaned_data"]
VIEW2 -->|"create_note(user, title, ...)"| SVC["services.py\ntransaction.atomic()\nNote.objects.create()\nnote.tags.set()"]
SVC -->|"INSERT"| DB
DB -->|"id=42"| SVC
SVC -->|"Note instance"| VIEW2
VIEW2 -->|"redirect(note_detail)"| RESP["HTTP 302\nLocation: /notes/42/"]
style SEL fill:#2C7A2C,color:#fff
style SVC fill:#0C4B33,color:#fff
style DB fill:#336791,color:#fff
18. Нормалізація — Before/After¶
Що показує: Чому дублювання в схемі — проблема, і як нормалізація вирішує її.
graph LR
subgraph "❌ До нормалізації (3NF)"
T1["employees\nid | name | dept_id | dept_name | dept_location | salary\n───────────────────────────────────────────────────\n1 | Alice| 10 | Engineering | Kyiv | 50000\n2 | Bob | 10 | Engineering | Kyiv | 45000\n3 | Carol| 20 | Design | Lviv | 48000"]
end
subgraph "✅ Після нормалізації (3NF)"
T2["employees\nid | name | dept_id FK | salary\n─────────────────────────────\n1 | Alice| 10 | 50000\n2 | Bob | 10 | 45000\n3 | Carol| 20 | 48000"]
T3["departments\nid | name | location\n──────────────────────────\n10 | Engineering | Kyiv\n20 | Design | Lviv"]
T2 -->|"dept_id → id"| T3
end
PROB["Engineering переїхала до Харкова:\n❌ UPDATE employees SET dept_location='Kharkiv'\n WHERE dept_name='Engineering'\n (1000 рядків!)\n\n✅ UPDATE departments SET location='Kharkiv'\n WHERE id=10\n (1 рядок!)"]
19. Relationship Types Map (повна карта)¶
Що показує: Всі типи зв'язків Django + SQL механізм + правило розташування FK.
graph TD
subgraph "OneToOneField 1:1"
U[User] <-->|"FK + UNIQUE\nuser_id → user.id\nOneToOneField cascade"| UP[UserProfile]
NOTE1["Вертикальне партиціонування\nUser: 5 основних полів\nUserProfile: 20 додаткових полів\nuser.profile ← related_name"]
end
subgraph "ForeignKey 1:N"
NB[Notebook] <---|"note.notebook_id → notebook.id\nForeignKey SET_NULL nullable"| NO[Note]
NOTE2["FK В NOTE (Many side)\nNotebook.notes.all() ← related_name\nнотатка без записника → NULL"]
end
subgraph "ManyToManyField M:N"
N[Note] <-->|"через автоматичну junction table\nnotes_app_note_tags\nnote_id, tag_id"| T[Tag]
NOTE3["Django автоматично:\nCREATE TABLE note_tags\nnote.tags.add(tag)\ntag.notes.all()"]
end
20. F() Expression — Race Condition Prevention¶
Що показує: Різницю між небезпечним Python read-modify-write та атомарним F().
sequenceDiagram
participant A as Process A
participant B as Process B
participant PG as PostgreSQL
Note over A,PG: ❌ НЕБЕЗПЕЧНО (Python RMW)
A->>PG: SELECT views_count FROM notes WHERE id=42
PG-->>A: views=100
B->>PG: SELECT views_count FROM notes WHERE id=42
PG-->>B: views=100
A->>PG: UPDATE notes SET views_count=101 WHERE id=42
B->>PG: UPDATE notes SET views_count=101 WHERE id=42
Note over A,PG: Результат: views=101 (999 переглядів загублено!)
Note over A,PG: ✅ БЕЗПЕЧНО F() Expression
A->>PG: UPDATE notes SET views_count=views_count+1 WHERE id=42
B->>PG: UPDATE notes SET views_count=views_count+1 WHERE id=42
Note over A,PG: PostgreSQL серіалізує: спочатку A, потім B
Note over A,PG: Результат: views=102 (правильно!)