TypeORM
Дальше: TypeScript и Node.js · Декораторы · Обработка ошибок
TypeORM — ORM для TypeScript/JavaScript: таблицы описывают классами-сущностями, запросы — через Repository и QueryBuilder. Типизация полей сущности проверяется TS; SQL и связи задаются декораторами или схемой Data API.
Маршрут: TypeScript и Node.js → TypeORM. Нужны декораторы и experimentalDecorators в legacy-стеке.
Альтернативы: Prisma (schema-first), Drizzle (SQL-like types). TypeORM выбран как распространённый в NestJS-стеке.
Что даёт ORM
| Без ORM | С TypeORM |
|---|---|
| SQL строки вручную | методы репозитория |
| Расхождение полей и таблицы | класс Entity |
| Миграции отдельно | CLI миграций (при настройке) |
TS не заменяет миграции и индексы — схему БД проектируют осознанно.
Минимальная сущность
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";
@Entity("users")
export class User {
@PrimaryGeneratedColumn("uuid")
id!: string;
@Column({ unique: true })
email!: string;
@Column()
name!: string;
}
Разбор:
@Entity("users")— имя таблицы.!— поле инициализирует БД/ORM, не конструктор — 23.md.
Инициализация DataSource
import { DataSource } from "typeorm";
import { User } from "./entity/User.js";
export const AppDataSource = new DataSource({
type: "postgres",
host: "localhost",
port: 5432,
username: "app",
password: "secret",
database: "app_db",
entities: [User],
synchronize: false, // только dev с осторожностью!
migrations: ["dist/migrations/*.js"],
});
await AppDataSource.initialize();
Разбор:
synchronize: trueудобно в dev, опасно в prod — используйте миграции.- Секреты — из env — 22.md.
Repository
const repo = AppDataSource.getRepository(User);
async function createUser(email: string, name: string): Promise<User> {
const user = repo.create({ email, name });
return repo.save(user);
}
async function findUser(id: string): Promise<User | null> {
return repo.findOneBy({ id });
}
Разбор:
findOneBy→User | null— обработайте null — 27.md.create+save— два шага; можноinsertдля bulk.
Связь one-to-many
@Entity("posts")
export class Post {
@PrimaryGeneratedColumn("uuid")
id!: string;
@Column()
title!: string;
@ManyToOne(() => User, (user) => user.posts)
author!: User;
}
@Entity("users")
export class User {
@PrimaryGeneratedColumn("uuid")
id!: string;
@OneToMany(() => Post, (post) => post.author)
posts!: Post[];
}
Разбор:
() => User— ленивая ссылка на класс (циклический import).- Загрузка связей:
relations: { posts: true }вfind.
DTO vs Entity
type CreateUserDto = { email: string; name: string };
async function register(dto: CreateUserDto): Promise<User> {
return createUser(dto.email, dto.name);
}
Не экспортируйте Entity напрямую в API-ответ — маппер toDto(user) — 22.md, 28.md.
Миграции
npx typeorm migration:generate -d src/data-source.ts src/migrations/AddUserIndex
npx typeorm migration:run -d src/data-source.ts
Разбор:
- Миграция — версионированное изменение схемы.
- Генерация сравнивает entities с БД — проверяйте SQL вручную.
Слой сервиса
export class UserService {
constructor(private readonly repo: Repository<User>) {}
async getOrThrow(id: string): Promise<User> {
const user = await this.repo.findOneBy({ id });
if (!user) throw new AppError(`User ${id}`, "NOT_FOUND");
return user;
}
}
Разделение: controller → service → repository — 22.md.
Частые ошибки
| Ошибка | Что делать |
|---|---|
synchronize: true в prod | миграции |
| Entity в JSON ответе | DTO |
| N+1 запросов | relations или join |
Нет reflect-metadata | import в entry |
any в QueryBuilder | типизировать параметры |
Практика
- Поднимите Postgres (Docker) и сущность
User. - Добавьте
PostсManyToOne/OneToMany. - Сгенерируйте и примените миграцию.
- Сервис
getOrThrowс доменной ошибкой. - Ответ API через DTO, не
Userentity.