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

Repository<Product> удобен для CRUD, но доменный сервис не обязан знать его API. Собственный репозиторий переводит доменные вызовы в persistence-операции и возвращает доменные объекты.

Порт вместо TypeORM в домене

interface ProductStore {
  byId(id: number): Promise<Product | null>;
  save(product: Product): Promise<void>;
}

class TypeOrmProductStore implements ProductStore {
  constructor(private readonly dataSource: DataSource) {}

  byId(id: number): Promise<Product | null> {
    return this.dataSource.manager.findOne(ProductEntity, { where: { id } })
      .then(row => row ? toDomain(row) : null);
  }

  async save(product: Product): Promise<void> {
    await this.dataSource.manager.save(ProductEntity, fromDomain(product));
  }
}

ProductEntity и Product могут совпадать в маленьком проекте, но разведение помогает не тащить декораторы и детали схемы в бизнес-правила.

Маппер toDomain/fromDomain преобразует типы и инварианты. Он не должен выполнять отдельные запросы ради маппинга каждого поля - иначе N+1 возвращается.

Не копировать Repository целиком

Собственный порт содержит только нужные сценарии операции: byId, save, activeBySeller. Методы delete, update, findAndCount не добавляют на будущее без использования.

EntityManager и QueryBuilder остаются внутри адаптера. Транзакционная граница принадлежит сценарию: репозиторий получает manager, а не открывает собственную транзакцию посреди операции.

Коротко

  • Порт выражает потребности домена, а не API ORM.
  • Entity и доменную модель разделяют через маппер при необходимости.
  • QueryBuilder и DataSource остаются в адаптере.
  • Порт не копирует универсальный CRUD.
  • Транзакцией владеет сценарий, а не отдельный метод репозитория.

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