Ось готовий урок, створений спеціально за твоїм запитом, у стилі енергійних лекцій CS50.
🎓 Урок: ORM у Microservices архітектурі
(або: Як не перетворити ваші сервіси на "розподілений моноліт")
1. 🔥 Вступ: проблема та мотивація
Уявіть, що ви живете у великому гуртожитку (це наш Моноліт). У вас є спільна кухня. Якщо ви хочете взяти сіль, ви просто йдете на кухню і берете її з полиці. Це просто, це швидко. Ваша ORM (Object-Relational Mapping) у моноліті працює саме так: один запит — і у вас є дані з таблиці Users, таблиці Orders і навіть Payments. Все поруч, все під рукою.
А тепер уявіть, що ви переїхали. Тепер кожен сервіс — це окремий приватний будинок на різних вулицях міста (це Мікросервіси).
Питання до вас: Чи можете ви просто зайти в будинок сусіда, відкрити його холодильник і взяти сіль? Звісно, ні! Двері зачинені. Вам треба подзвонити (зробити HTTP-запит) і попросити господаря винести вам сіль.
У чому тут проблема?
Більшість розробників намагаються використовувати ORM у мікросервісах так само як у моноліті. Вони хочуть написати user.getOrders(), але забувають, що User живе в одному будинку (базі даних), а Orders — у зовсім іншому, на іншому сервері.
Якщо ви не зрозумієте, як змінити підхід до ORM тут, ви отримаєте систему, яка працює повільно, падає від найменшого "чхання" мережі та перетворюється на нічний кошмар для підтримки.
Сьогодні ми навчимося, як не допустити цього.
2. 🧠 Теоретична база (без сухої академічності)
Давайте розберемося, що відбувається "під капотом".
Що таке ORM (нагадування)
ORM — це перекладач. Ви говорите мовою об’єктів (Python, Java, C#), а база даних розуміє лише SQL. ORM перетворює user.save() на INSERT INTO users....
Головне правило мікросервісів: Database per Service
Це священний грааль. Кожен мікросервіс має свою власну базу даних. Сервіс "Замовлення" не має права лізти в базу "Користувачі" напряму через SQL-JOIN. Ніколи.
Як змінюється роль ORM?
У моноліті ORM — це "павук", який плете величезну павутину зв'язків (OneToMany, ManyToMany) через всю базу даних.
У мікросервісах ORM стає інтровертом.
Що треба запам’ятати назавжди:
1. Bounded Context (Обмежений контекст): Ваша модель ORM описує тільки те, що живе в цьому конкретному сервісі.
2. Ніяких Foreign Keys між базами: Ви не можете зробити зовнішній ключ на таблицю, яка фізично знаходиться на іншому сервері.
3. Зв'язок через ID: Замість об'єкта User у моделі Order ви будете зберігати лише сухий user_id (просто число або UUID).
Інтуїтивне розуміння: Думайте про ORM у мікросервісі як про записник, який лежить виключно у вашій кишені. Ви записуєте туди свої справи. Якщо вам треба дізнатися справи друга, ви не читаєте його записник, ви питаєте його напряму.
3. 🧪 Приклади (від простого до реального)
Приклад 1: Як ми звикли (Монолітний підхід — ❌ ТУТ НЕ ПРАЦЮЄ)
Уявіть класичний код на Django/Hibernate/Entity Framework.
# Моноліт. Все в одній БД.
class Order(Model):
id = IntegerField()
product = StringField()
# Магія ORM: пряме посилання на об'єкт
user = ForeignKey(User, related_name='orders')
# Використання:
my_order = Order.objects.get(id=1)
print(my_order.user.email) # ORM сама робить JOIN або додатковий SELECT
Чому це впаде у мікросервісах? Тому що таблиці User у базі сервісу замовлень просто не існує!
Приклад 2: Реальність мікросервісів (Розрив зв'язків — ✅ ПРАВИЛЬНО)
Ми розділили систему на UserService та OrderService.
Код у сервісі замовлень (OrderService):
# Тільки локальні дані!
class Order(Model):
id = IntegerField()
product = StringField()
# Ніякого ForeignKey на User! Тільки ID.
user_id = UUIDField()
# Ніяких методів типу .get_user() всередині моделі ORM!
Питання до студента: Як нам тепер отримати email користувача, якщо у нас є тільки user_id, а база користувачів десь далеко?
Відповідь: Ми робимо це на рівні коду (бізнес-логіки), а не бази даних.
# Логіка в OrderService
def get_order_details(order_id):
# 1. ORM дістає замовлення зі своєї локальної БД
order = OrderRepository.get(order_id)
# 2. Робимо HTTP запит до UserService
user_data = http_client.get(f"http://users-service/users/{order.user_id}")
# 3. "Клеїмо" дані вручну (або через DTO)
return {
"product": order.product,
"user_email": user_data["email"]
}
Приклад 3: Складніший (Дублювання даних для швидкості)
А що, якщо UserService лежить? Ми не зможемо показати замовлення? Або що, якщо нам треба показати список із 100 замовлень? Робити 100 HTTP запитів — це вбивство продуктивності.
Рішення: Денормалізація.
Ми трохи змінюємо нашу ORM модель у сервісі замовлень:
class Order(Model):
id = IntegerField()
product = StringField()
user_id = UUIDField()
# УВАГА! Ми зберігаємо копію email прямо тут, у базі замовлень.
# Це порушує правила нормалізації, але рятує мікросервіси.
user_email_snapshot = StringField()
Тепер ORM знову працює швидко, бо дані локальні. Але виникає проблема синхронізації (про це — в наступних темах про черги повідомлень).
4. 🛠 Практична частина
Спробуймо застосувати це на практиці. Уявіть, що ви будуєте аналог Uber. У вас є сервіс Drivers (водії) і сервіс Trips (поїздки).
Завдання 1: Розділення моделей
Напишіть (псевдокодом) структуру моделі Trip для мікросервісу поїздок. Пам'ятайте, вона не може посилатися на клас Driver. Вона повинна мати driver_id.
Завдання 2: "А де мої дані?"
Ви отримали з бази Trip об'єкт: {id: 101, driver_id: 555, route: "Kyiv-Lviv"}.
Напишіть функцію, яка повертає повну інформацію про поїздку, включно з ім'ям водія (припустимо, є функція fetch_driver(id)).
Завдання 3: Кейс на помилку (Find the bug) Студент-стажер написав такий код у мікросервісі Trips:
# Сервіс Trips
trips = Trip.objects.filter(status="active")
for trip in trips:
# Запит до сервісу Drivers
driver = http_client.get(f"http://drivers-service/{trip.driver_id}")
print(f"Водій {driver.name} їде.")
Питання: Якщо у нас 500 активних поїздок, що станеться з мережею і сервісом Drivers? Як це називається? (Підказка: N+1 проблема, але розподілена).
Завдання 4: Міні-кейс Вам потрібно фільтрувати поїздки за рейтингом водія. Рейтинг живе в базі Drivers, а поїздки — в базі Trips. Як це зробити, не викачуючи всі поїздки в пам'ять? (Запропонуйте ідею: дублювання рейтингу в Trips або щось інше).
5. 💡 Мислення як у розробника
Як відрізнити новачка від профі в цій темі?
⛔️ Новачок: * Намагається створити "спільну бібліотеку" (Shared Library), де лежать усі ORM-моделі системи, і підключає цю бібліотеку до всіх сервісів. * Чому це погано: Змінив поле в моделі для одного сервісу — зламав усі інші. Ви створили "розподілений моноліт".
⛔️ Новачок: * Намагається робити транзакції між сервісами. "Якщо я зберіг замовлення, але не зміг зняти гроші — відкоти збереження замовлення через ORM". * Реальність: Розподілені транзакції — це зло.
✅ Досвідчений інженер: * Думає контрактами (API), а не таблицями. Йому байдуже, як дані лежать у чужій базі. * Приймає надлишковість. Краще зберегти копію імені користувача в замовленні, ніж щоразу "смикати" інший сервіс. * DTO (Data Transfer Objects). Використовує проміжні об'єкти для передачі даних, ніколи не віддає "сирі" ORM-об'єкти назовні через API.
Порада з практики: Якщо вам потрібно зробити складний звіт, де збираються дані з 5 мікросервісів — не робіть це через ланцюжок HTTP-запитів у циклі. Використайте окремий сервіс аналітики, куди дані стікаються у фоновому режимі, і робіть SELECT вже там.
6. 🧩 Підсумок
Отже, що ми маємо в сухому залишку?
- ORM у мікросервісах — це локальний інструмент. Він керує тільки своєю маленькою базою.
- Ніяких JOIN-ів між сервісами. Забудьте про це.
- ID — це клей. Ми пов'язуємо світи через ідентифікатори (UUID/Int), а не через об'єкти.
- Мережа — це вузьке місце. Уникайте N+1 запитів через HTTP.
Тепер ви вмієте: Проєктувати моделі даних так, щоб сервіси залишалися незалежними. Ви більше не намагатиметеся "натягнути" монолітні звички на розподілену систему.
🔍 Що далі? Ви, мабуть, помітили проблему: "А що, якщо я зберіг копію email користувача в замовленні, а користувач змінив пошту? У мене тепер старі дані!". Саме про це ми поговоримо на наступному уроці: Event-Driven Architecture та синхронізація даних через Kafka/RabbitMQ.
А поки що — щасти вам із кодом!