← назад к разделу

Связь 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 и проекцией, а не параллельными запросами.
  • Большие коллекции не загружают по умолчанию.
  • План и число запросов проверяют на реальном сценарии.

Что почитать дальше