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

NESTJS · TYPEORM Фаза 7. Персистентность TypeORM

Персистентность: TypeORM

TypeScript · NestJS · PostgreSQL

DataSource, сущности, репозитории, транзакции и миграции схемы.

1. DataSource подключает приложение к БД

1 / 5

DataSource хранит настройки соединения и создаёт entity manager. TypeOrmModule регистрирует его в DI-контейнере.

  • Соединение переиспользуется.
  • Настройки приходят из ConfigService.
  • Pool ограничивает одновременные запросы.
TypeOrmModuleconfigured DataSourceentity managerconnection pool Repositoriesinjected

2. Entity отображает таблицу и связи

2 / 5

@Entity задаёт таблицу, @PrimaryGeneratedColumn идентификатор, @Column поля, декораторы relations описывают связи.

  • Entity - persistence model, не DTO.
  • Типы колонок соответствуют БД.
  • Связи загружаются явно.
OrderEntity → orders id: number customerId: string items → OneToMany

3. Репозиторий отвечает за persistence queries

3 / 5

@InjectRepository внедряет репозиторий. Find, save и query builder остаются инфраструктурой, а доменные решения - в use case.

  • Не прячьте бизнес-правила в SQL.
  • Query builder параметризуется.
  • Репозиторий не заменяет транзакцию.
OrderServicebusiness use case OrderRepositoryfind + savequery builder PostgreSQL

4. Транзакция связывает изменения атомарно

4 / 5

Заказ и outbox сохраняются одной транзакцией. При ошибке rollback отменяет обе записи, после commit событие публикуется отдельно.

  • Граница транзакции короткая.
  • Не вызывайте HTTP внутри.
  • Ошибка не оставляет частичное состояние.
DB transactionsave(order)save(outbox)commit / rollback Publisher Broker

5. Миграции развивают схему через код

5 / 5

Migration описывает up и down, применяется через DataSource и записывает историю. Схема не меняется вручную незаметно для приложения.

  • Миграции версионируются.
  • Тест накатывает предыдущую схему.
  • Не используйте synchronize на проде.
V1 schema V2 index V3 outbox Migration historyapplied versions

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

NestJS не умеет работать с базой данных из коробки — это задача отдельной библиотеки. В Node-экосистеме стандарт для NestJS — TypeORM: он интегрируется с системой модулей и внедрения зависимостей NestJS, а сущности описываются декораторами в том же стиле, что и весь остальной код.

Проблема: как связать NestJS и базу данных

Без ORM пришлось бы вручную открывать соединения, писать SQL-строки по всему коду и самостоятельно превращать строки из базы в объекты. При малейшем изменении таблицы пришлось бы искать все места в коде.

TypeORM решает это: вы описываете таблицу как TypeScript-класс, а библиотека берёт на себя соединение, генерацию SQL и маппинг данных в объекты.

Подключение: DataSource и модули

TypeOrmModule.forRoot поднимает соединение с базой при старте приложения. Его регистрируют один раз в корневом модуле:

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      url: process.env.DATABASE_URL,
      entities: [Product],
      migrationsRun: true,
    }),
  ],
})
export class AppModule {}

На практике строку подключения берут не напрямую из process.env, а через ConfigService — для единообразия с остальными настройками. Для этого используют forRootAsync:

TypeOrmModule.forRootAsync({
  imports: [ConfigModule],
  useFactory: (config: ConfigService) => ({
    type: 'postgres',
    url: config.get('DATABASE_URL'),
    entities: [Product],
    migrationsRun: true,
  }),
  inject: [ConfigService],
}),

Сущности: описание таблиц через декораторы

Сущность — это обычный TypeScript-класс с декоратором @Entity. Каждое поле класса соответствует колонке в таблице:

import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

@Entity('products')
export class Product {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @Column('int')
  price: number;
}

@PrimaryGeneratedColumn() означает автоинкрементный первичный ключ. @Column('int') задаёт тип колонки явно — это важно, потому что TypeORM иначе выводит тип из TypeScript, и результат не всегда совпадает с ожидаемым.

Репозитории: доступ к данным

Репозиторий — объект, через который идут все запросы к конкретной таблице. TypeORM создаёт его автоматически для каждой сущности; в NestJS его получают через @InjectRepository:

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Product } from './product.entity';

@Injectable()
export class ProductRepository {
  constructor(
    @InjectRepository(Product) private readonly repo: Repository<Product>,
  ) {}

  findById(id: number): Promise<Product | null> {
    return this.repo.findOne({ where: { id } });
  }

  save(product: Product): Promise<Product> {
    return this.repo.save(product);
  }
}

Чтобы @InjectRepository(Product) заработал, нужно зарегистрировать сущность в модуле через TypeOrmModule.forFeature:

@Module({
  imports: [TypeOrmModule.forFeature([Product])],
  providers: [ProductRepository],
})
export class ProductsModule {}

Обёртка над Repository<Product> прячет детали ORM за чётким интерфейсом. Это значит, что остальной код не знает, откуда берутся данные — хоть из базы, хоть из теста с заглушкой.

Транзакции

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

Удобный способ в TypeORM — DataSource.transaction. Всё, что выполняется внутри коллбэка, попадает в одну транзакцию. Успех → коммит, исключение → откат:

import { Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';

@Injectable()
export class CreateProductHandler {
  constructor(private readonly dataSource: DataSource) {}

  async handle(command: CreateProductCommand): Promise<Product> {
    return this.dataSource.transaction(async (manager) => {
      const product = manager.create(Product, {
        name: command.name,
        price: command.price,
      });
      return manager.save(product);
    });
  }
}

DataSource внедряется как обычный провайдер — NestJS автоматически делает его доступным после TypeOrmModule.forRoot.

Важный принцип: решение «зафиксировать или откатить» принимает тот, кто владеет сценарием, а не репозиторий на каждом отдельном save.

Миграции: версионирование схемы

Каждое изменение схемы базы данных должно быть зафиксировано как миграция — файл с SQL-инструкциями, который хранится в репозитории и выполняется один раз.

Миграции в TypeORM запускаются автоматически при старте (migrationsRun: true) или вручную через CLI:

# сгенерировать миграцию по изменениям сущностей
npx typeorm migration:generate src/migrations/AddProductTable -d src/data-source.ts

# применить миграции
npx typeorm migration:run -d src/data-source.ts

# откатить последнюю
npx typeorm migration:revert -d src/data-source.ts

Автогенерацию всегда проверяют руками перед применением — TypeORM может добавить лишние операции.

Параметр synchronize: true автоматически подгоняет схему базы под сущности при каждом старте. Это удобно для локальных экспериментов, но категорически нельзя в работающем сервисе: он может молча удалить колонку или таблицу вместе с данными.

Коротко

  • TypeOrmModule.forRoot — единственное место подключения к базе; forFeature регистрирует сущности в каждом модуле.
  • Сущность — класс с @Entity и декораторами колонок; TypeORM сам генерирует SQL.
  • Репозиторий внедряется через @InjectRepository и прячет ORM за интерфейсом.
  • DataSource.transaction — простой способ выполнить несколько операций атомарно.
  • Схему меняют только миграциями; synchronize: true — только локально.
  • Автогенерированные миграции всегда проверяют перед применением.

Что пощупать

В стартовом каталоге практикума remodov/marketplace-system-nodejs сущность TypeORM держит поля закрытыми, а колонки numeric и bigint проходят через трансформеры в Decimal и number. Чтение идёт через QueryBuilder, запись - сырым SQL с условием WHERE version = $5: ноль затронутых строк означает параллельное изменение и ConflictError. Миграции лежат в коде и накатываются при старте.

Код: product.entity.ts, product.repository.ts, миграции.

Сделаем сами

Ветка step-02-read-endpoint - метод репозитория с условием по цене и сортировкой по колонке.

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