Appearance
ADR-001: Выбор ORM / Query Builder
Дата: 2026-01-04
Статус: Принято
Авторы: Backend Team
Контекст
Проект «Маркетплейс автозапчастей СПб» использует PostgreSQL 16.x в качестве основной СУБД. Необходимо выбрать инструмент для работы с базой данных, который обеспечит:
- Типобезопасность (проект на TypeScript)
- Поддержку сложных запросов (JOIN через 5+ таблиц, CTE, оконные функции)
- Интеграцию с Medusa.js 2.x
- Поддержку миграций
- Работу с PostGIS для геопространственных запросов
Структура данных проекта
Согласно autoparts-contracts-v5.md, система включает 21 таблицу со сложными связями:
- M:N связи:
products ↔ categories,oem_numbers ↔ oem_cross_references - Иерархии:
car_makes → car_models → car_generations - Геоданные:
regions → cities → districts/metro_stations - Денормализованные счётчики с триггерами
Рассмотренные альтернативы
| Решение | Типобезопасность | Сложные запросы | Medusa.js | Миграции | PostGIS |
|---|---|---|---|---|---|
| MikroORM | ✅ Excellent | ✅ QueryBuilder | ✅ Встроен | ✅ | ⚠️ Плагин |
| Prisma | ✅ Excellent | ⚠️ Ограничен | ❌ Замена | ✅ | ⚠️ Raw SQL |
| TypeORM | ⚠️ Частичная | ✅ QueryBuilder | ❌ Legacy | ✅ | ✅ |
| Drizzle | ✅ Excellent | ✅ SQL-like | ❌ Замена | ✅ | ⚠️ Raw SQL |
| Knex.js | ❌ Нет | ✅ Любые | ❌ Замена | ✅ | ✅ |
Решение
Используем MikroORM — ORM, встроенный в Medusa.js 2.x.
Обоснование
Нативная интеграция с Medusa.js
- Medusa 2.x использует MikroORM как основной ORM
- Все модули Medusa (Products, Customers, Orders) уже работают через MikroORM
- Единообразие: кастомные модули используют тот же инструмент
Типобезопасность
typescript// Полная типизация сущностей и отношений @Entity() class Product { @ManyToMany(() => Category) categories = new Collection<Category>(this); @OneToMany(() => ProductCompatibility, pc => pc.product) compatibility = new Collection<ProductCompatibility>(this); }Мощный QueryBuilder для сложных запросов
typescript// Поиск товаров с совместимостью и категориями const products = await em.createQueryBuilder(Product, 'p') .select('p.*') .leftJoinAndSelect('p.categories', 'c') .leftJoin('p.compatibility', 'pc') .where({ 'pc.make_id': makeId }) .andWhere({ 'p.steering': { $in: ['left', 'universal'] } }) .andWhere({ 'p.status': 'active' }) .orderBy({ 'p.created_at': 'DESC' }) .limit(20) .getResultList();Поддержка Raw SQL для PostGIS
typescript// Геопоиск в радиусе const nearbyProducts = await em.execute(` SELECT p.*, ST_Distance(c.location, ST_SetSRID(ST_MakePoint($1, $2), 4326)) / 1000 as distance_km FROM products p JOIN users u ON p.seller_id = u.id JOIN cities c ON u.city_id = c.id WHERE ST_DWithin(c.location, ST_SetSRID(ST_MakePoint($1, $2), 4326), $3 * 1000) ORDER BY distance_km ASC `, [lon, lat, radiusKm]);Unit of Work паттерн
- Автоматическое отслеживание изменений
- Batch-операции для производительности
- Транзакции из коробки
Паттерны использования
Кастомные модули Medusa
typescript
// src/modules/cars/models/car-make.ts
import { model } from "@medusajs/framework/utils";
export const CarMake = model.define("car_make", {
id: model.id().primaryKey(),
name: model.text(),
slug: model.text().unique(),
logo_url: model.text().nullable(),
models: model.hasMany(() => CarModel),
});Репозитории
typescript
// src/modules/cars/repositories/car-repository.ts
@Injectable()
export class CarRepository {
constructor(
@InjectRepository(CarMake)
private readonly makeRepo: EntityRepository<CarMake>,
) {}
async findMakesWithModels(): Promise<CarMake[]> {
return this.makeRepo.findAll({
populate: ['models', 'models.generations'],
orderBy: { name: 'ASC' },
});
}
}Миграции
bash
# Генерация миграции
npx medusa db:generate car_makes_table
# Применение миграций
npx medusa db:migrateПоследствия
Положительные
- Консистентность: единый ORM для всего проекта
- Документация: примеры Medusa применимы напрямую
- Поддержка: активное сообщество Medusa и MikroORM
- Производительность: Identity Map, lazy loading, batch inserts
- Гибкость: QueryBuilder + Raw SQL для edge cases
Отрицательные
- Кривая обучения: MikroORM менее популярен, чем Prisma/TypeORM
- PostGIS: нет нативной поддержки, требуется raw SQL
- Миграции: менее удобны, чем в Prisma
Риски и митигация
| Риск | Митигация |
|---|---|
| Сложные геозапросы | Выделить в отдельный сервис с raw SQL |
| Производительность M:N | Использовать populate выборочно, добавить индексы |
| Сложность отладки | Включить логирование SQL в dev-окружении |