Ми розпочали цей модуль із концептуального моделювання — навмисно технологічно-агностичного. Ми виявляли сутності, зв'язки та атрибути аудіоплатформи, не прив'язуючись до жодної конкретної системи зберігання. Лише починаючи з другої статті ми обрали реляційну модель і послідовно рухалися до фізичної SQL-схеми та Flyway-міграцій.
Цей вибір був усвідомленим і обґрунтованим: реляційна модель з її суворими гарантіями цілісності, зрілою екосистемою та десятиліттями виробничого досвіду залишається найбезпечнішим вибором за замовчуванням для більшості бізнес-задач. Але чи є вона єдиним можливим вибором для нашої аудіоплатформи?
Відповідь — ні. І в цьому полягає ключова педагогічна мета фінальної статті модуля: повернутися до концептуальної моделі і подивитися, як та сама предметна область могла б виглядати у принципово інших парадигмах зберігання.
Такий погляд корисний з кількох причин. По-перше, він поглиблює розуміння реляційної моделі через контраст: коли бачиш, як те саме завдання вирішується по-іншому, починаєш краще розуміти, чому реляційна модель зробила саме такі вибори. По-друге, сучасні виробничі системи рідко є монолітними: вони поєднують кілька СУБД різних типів — концепція, відома як Polyglot Persistence (поліглотне зберігання). Архітектор, що знає лише SQL, опиниться в безпорадності перед задачею вибору між MongoDB та Cassandra.
У реляційній моделі аудіокнига «розкидана» по кількох таблицях: сама книга — в audiobooks, автор — в authors, жанр — в genres, файли — в audiobook_files. Щоб отримати повну картину, потрібен JOIN з чотирьох таблиць.
Документо-орієнтована модель (Document-Oriented Model), яку реалізують MongoDB, CouchDB та інші, пропонує протилежний підхід: зберігати пов'язані дані разом, в одному документі. Документ — це самодостатня одиниця даних, зазвичай у форматі JSON або BSON.
SELECT
a.title,
a.duration,
au.first_name || ' ' || au.last_name AS author,
g.name AS genre,
af.file_path,
af.format
FROM audiobooks a
JOIN authors au ON a.author_id = au.id
JOIN genres g ON a.genre_id = g.id
JOIN audiobook_files af ON af.audiobook_id = a.id
WHERE a.id = '770e8400-...';
// db.audiobooks.findOne({ _id: ObjectId("770e8400...") })
// Результат — один документ, що містить усе:
{
"_id": "770e8400-e29b-41d4-a716-446655440001",
"title": "Космічна подорож",
"duration": 7200,
"releaseYear": 2023,
"author": {
"firstName": "Іван",
"lastName": "Коваль",
"bio": "Сучасний поет і прозаїк..."
},
"genre": {
"name": "Фантастика",
"description": "Наукова фантастика, фентезі..."
},
"files": [
{
"filePath": "/audio/kosmichna_podorozh.mp3",
"format": "mp3",
"size": 150000000
}
]
}
Результат очевидний: для читання повної інформації про аудіокнигу документна модель потребує одного звернення до сховища. У реляційній моделі — мінімум чотирьох JOIN.
Проте не все так однозначно. У документній моделі існує фундаментальний вибір: вбудовувати (embed) пов'язані дані безпосередньо в документ чи посилатися (reference) на інші документи через ідентифікатор.
Embedding (вбудовування)
Дані фізично розміщуються всередині батьківського документа.
Переваги:
Коли доцільно:
Приклад: audiobook.files[] — файли книги не існують окремо від книги.
Referencing (посилання)
Документ зберігає лише ідентифікатор пов'язаного документа.
Переваги:
Коли доцільно:
Приклад: user.collectionIds[] — користувач має багато колекцій.
UPDATE authors SET last_name = '...' WHERE id = '...'. Документна модель торгує консистентністю заради швидкості читання.У реляційній та документній моделях первинними об'єктами є сутності — рядки або документи. Зв'язки між ними виражаються вторинно: через FK або масиви ідентифікаторів. Але існує клас задач, де саме зв'язки між об'єктами є найціннішою інформацією, а не властивості самих об'єктів.
Класичний приклад — рекомендаційні системи. «Користувачі, що слухали цю книгу, також слухали...» — це питання не про властивості книги, а про мережу поведінки користувачів. Реляційна БД відповідає на таке питання через серію JOIN зростаючої складності. Графова БД (Graph Database), наприклад Neo4j, відповідає на нього природно, бо зв'язки є першокласними об'єктами моделі.
Графова модель складається з двох базових елементів:
Author, Audiobook, User, Genre.WROTE, LISTENS_TO, BELONGS_TO, ADDED_TO.Помітьте, наскільки природно наша концептуальна ER-модель відображається на граф. Кожна сутність — вузол, кожен зв'язок — ребро. Ребра є спрямованими і мають тип (label), а також можуть нести власні властивості — так само як атрибути зв'язку в концептуальній моделі.
Neo4j використовує декларативну мову запитів Cypher. Її синтаксис навмисно нагадує ASCII-арт: вузли позначаються (), ребра — --> або -[:TYPE]->.
-- Які книги написав Іван Коваль?
MATCH (auth:Author {lastName: "Коваль"})-[:WROTE]->(a:Audiobook)
RETURN a.title, a.duration
ORDER BY a.releaseYear DESC;
-- Книги, що їх слухали люди зі схожим смаком:
MATCH (u:User {username: "oleksandr23"})-[:LISTENS_TO]->(a:Audiobook)
<-[:LISTENS_TO]-(similar:User)-[:LISTENS_TO]->(rec:Audiobook)
WHERE NOT (u)-[:LISTENS_TO]->(rec)
RETURN rec.title, count(similar) AS commonListeners
ORDER BY commonListeners DESC
LIMIT 5;
-- Через які жанри проходять книги автора?
MATCH (auth:Author)-[:WROTE]->(a:Audiobook)-[:BELONGS_TO]->(g:Genre)
WHERE auth.lastName = "Коваль"
RETURN auth.firstName, g.name, count(a) AS bookCount;
Зверніть на другий запит: реалізація рекомендаційного алгоритму «люди зі схожим смаком слухали також...» у Cypher займає п'ять рядків. Аналогічний запит у SQL вимагав би кількох рівнів вкладених підзапитів або WITH-виразів і був би значно складнішим для читання та оптимізації. Це і є природна перевага графових БД: обхід зв'язків є першокласною операцією, а не вторинним ефектом JOIN.
Author, Audiobook, User, Genre стали вузлами. Зв'язки writes, listens_to, belongs_to стали ребрами. Атрибути зв'язку Listening_Progress (position, last_listened) стають властивостями ребра LISTENS_TO. Концептуальне моделювання є дійсно технологічно-агностичним — воно однаково добре відображається і на SQL, і на граф.Key-Value сховища (Key-Value Store) реалізують найпростішу можливу модель: ключ → значення. Немає таблиць, колекцій, вузлів, схем. Є лише словник, де за рядковим ключем можна зберегти й отримати значення.
Redis — найпопулярніше key-value сховище у світі. Воно зберігає дані в оперативній пам'яті, що забезпечує надзвичайно швидке читання і запис (мікросекунди). Redis підтримує різні типи значень: рядки, хеші, списки, множини, відсортовані множини.
1. Кешування прогресу слухання — найгарячіші дані платформи.
Таблиця listening_progresses в PostgreSQL оновлюється щоразу, коли користувач слухає книгу — а це може бути кожні 10–30 секунд. Для 1000 одночасних слухачів це 33–100 записів у секунду. Redis поглинає таке навантаження без зусиль:
Ключ: "progress:{user_id}:{audiobook_id}"
Значення: "3600" ← позиція у секундах
TTL: 86400 ← автовидалення через 24 год, якщо не оновлено
Раз на хвилину (або при виході з сесії) додаток синхронізує Redis-дані назад у PostgreSQL. «Холодне» PostgreSQL не перевантажується частими UPDATE, а Redis обробляє «гарячі» оновлення.
2. Кешування каталогу — прискорення читання популярних даних.
Список жанрів змінюється рідко (статичний довідник), але читається при кожному завантаженні головної сторінки. Кешуємо в Redis із TTL 1 годину — база даних отримує запит щогодини замість щосекунди.
3. Атомарні лічильники — без блокувань та перегонів.
-- Атомарне збільшення лічильника відтворень:
INCR "plays:770e8400-e29b-41d4-a716-446655440001"
Команда INCR є атомарною — навіть при одночасних запитах від тисяч користувачів кожне збільшення зараховується коректно без транзакцій або блокувань рядків.
4. Управління сесіями — зберігання JWT або сесійних токенів.
SET "session:{token}" "{user_id}"
EXPIRE "session:{token}" 1800 ← 30 хвилин
Чотири розглянуті моделі не є конкурентами — вони є спеціалізованими інструментами для різних класів задач. Таблиця нижче структурує ключові відмінності з точки зору архітектурного вибору:
| Критерій | Реляційна (PostgreSQL/H2) | Документна (MongoDB) | Графова (Neo4j) | Key-Value (Redis) |
|---|---|---|---|---|
| Модель даних | Таблиці, рядки, стовпці | JSON-документи | Вузли та ребра | Ключ → значення |
| Схема | Жорстка (DDL) | Гнучка (schema-less) | Гнучка | Немає схеми |
| Зв'язки | FK + JOIN | Embedding / Reference | Першокласні об'єкти | Немає |
| Транзакції (ACID) | ✅ Повна підтримка | ⚠️ Часткова (з v4.0) | ⚠️ Часткова | ❌ Лише базова |
| Гнучкість схеми | Низька (потрібні міграції) | Висока | Висока | Максимальна |
| Швидкість читання | Висока при індексах | Дуже висока (немає JOIN) | Висока для обходу графа | Екстремальна (in-memory) |
| Швидкість запису | Висока | Висока | Середня | Екстремальна |
| Складні JOIN-запити | ✅ Природні | ❌ Відсутні | ✅ Через обхід | ❌ Відсутні |
| Рекомендації | ⚠️ Складно | ⚠️ Складно | ✅ Природні | ❌ Не підходить |
| Кешування | ❌ Не призначена | ❌ Не призначена | ❌ Не призначена | ✅ Ідеальна |
| Аудіоплатформа | Core data (каталог, юзери) | Каталог з вбудованими даними | Рекомендації | Прогрес, кеш, сесії |
SELECT.Жодна з розглянутих моделей не є «найкращою» у вакуумі — кожна є найкращою для свого класу задач. Сучасна архітектура не змушує обирати одну: замість цього вона поєднує кілька спеціалізованих сховищ, кожне з яких використовується там, де воно природно. Цей підхід отримав назву Polyglot Persistence (поліглотне зберігання) — термін, введений Мартіном Фаулером у 2011 році.
Аналогія: у кулінарії не існує одного «найкращого» ножа. Кухонний ніж для овочів, ніж для хліба, ніж для м'яса — кожен спроектований для своєї задачі. Досвідчений кухар знає, який ніж взяти, не намагаючись різати хліб ножем для м'яса.
Розглянемо, як виглядала б виробнича архітектура аудіоплатформи з використанням кількох сховищ:
У цій архітектурі кожне сховище виконує свою спеціалізовану роль:
LIKE '%keyword%' не масштабується; Elasticsearch — так.Дано сутність Audiobook з атрибутами: id, title, duration, releaseYear, author (вкладений об'єкт із firstName, lastName), genres (масив рядків), files (масив об'єктів із filePath, format).
Завдання:
Для кожного з наведених сценаріїв оберіть найбільш відповідне сховище (SQL, Document, Graph, або Key-Value) та обґрунтуйте вибір:
Ви проектуєте архітектуру нової платформи для онлайн-курсів (courses, lessons, instructors, students, progress, certificates).
Завдання:
Ми розпочали цей модуль із концептуальної моделі, що не залежала від жодної технології зберігання. І повернулися до неї в кінці — щоб переконатися, що ця незалежність була не формальним прийомом, а відображенням реального факту: одна й та сама предметна область може бути реалізована в принципово різних парадигмах.
Реляційна модель залишається найуніверсальнішим і найбезпечнішим вибором за замовчуванням. Але усвідомлений вибір — це той, що зроблений з розумінням альтернатив. Архітектор, що знає лише SQL, вирішуватиме задачу рекомендацій через складні JOIN-запити, де Neo4j вирішив би її п'ятьма рядками Cypher. Той самий архітектор намагатиметься зберігати прогрес слухання у PostgreSQL, де Redis впорався б у тисячу разів швидше.
SQL — каркас системи
Document — зручна агрегація
Graph — мережа зв'язків
Key-Value — швидкість
Цей модуль завершено. Ви пройшли повний шлях від концептуальної ER-моделі через нормалізацію, фізичну DDL-схему та Flyway-міграції — і тепер маєте як практичні навички реляційного проектування, так і архітектурне розуміння ширшого ландшафту технологій зберігання даних.
Database Migrations: Версіонування схеми з Flyway
Схема бази даних — це живий артефакт. Вивчаємо Flyway: версіоновані та повторювані міграції, naming convention, підключення до Java-проєкту, стратегії розгортання та інтеграція з CI/CD.
Object-Relational Impedance Mismatch: Два світи, що не хочуть дружити
Фундаментальна невідповідність між об'єктно-орієнтованою та реляційною парадигмами. П'ять ключових розбіжностей, ручний маппінг через JDBC та дорожня карта патернів, які вирішують цю проблему.