Связь entity позволяет читать продавца вместе с товаром, но не обещает бесплатный план запроса. Отдельно решают, какая связь нужна и когда ее загружать.
Владелец связи
@ManyToOne(() => Seller, seller => seller.products, {
nullable: false,
eager: false,
})
@JoinColumn({ name: 'seller_id' })
seller: Seller;
ManyToOne находится там, где лежит внешний ключ. @JoinColumn называет колонку. eager: false отключает автоматическую загрузку связи при каждом чтении товара.
Обратная сторона OneToMany удобна для навигации по модели, но коллекцию продавца не стоит загружать без необходимости: она может быть большой.
Lazy loading требует явного запроса
Если связь не загружена, обращение к product.seller не должно молча превращаться в серию запросов. В TypeORM нужные relations указывают явно:
const product = await repo.findOne({
where: { id: 42 },
relations: { seller: true },
});
Либо делают join через QueryBuilder. Важно, чтобы сервисный слой понимал, какая проекция нужна сценарию: только товар, товар с продавцом или список без связей.
N+1 появляется в цикле
Типичный антипаттерн: сначала загрузить сто товаров, затем для каждого отдельно запросить продавца. Получается один запрос на товары и сто на связи.
Решение - один join с выбором нужных полей. Для больших списков проверяют SQL и число запросов, а не только итоговый JSON.
Promise.all по репозиториям в цикле не убирает N+1: он лишь выполняет много запросов параллельно и может исчерпать пул.
Коротко
ManyToOneиJoinColumnнаходятся у владельца внешнего ключа.- Нужные relations загружают явно через
relationsили join. - N+1 исправляют join и проекцией, а не параллельными запросами.
- Большие коллекции не загружают по умолчанию.
- План и число запросов проверяют на реальном сценарии.
Что почитать дальше
- QueryBuilder - join и проекции.
- Производительность и ошибки - как увидеть проблему до production.