Настройка ORM TypeORM для веб-приложения
TypeORM — один из старейших ORM для TypeScript/Node.js. Поддерживает Active Record и Data Mapper паттерны, декораторы для определения сущностей и богатый набор функций: миграции, subscribers, relations, query builder. Широко используется в NestJS-проектах — там это де-факто стандарт.
Установка
npm install typeorm reflect-metadata
npm install pg # PostgreSQL
# или mysql2, better-sqlite3, mongodb
# В tsconfig.json
{
"compilerOptions": {
"emitDecoratorMetadata": true,
"experimentalDecorators": true
}
}
Подключение к базе
// db/data-source.ts
import 'reflect-metadata'
import { DataSource } from 'typeorm'
import { User } from './entities/User'
import { Post } from './entities/Post'
export const AppDataSource = new DataSource({
type: 'postgres',
url: process.env.DATABASE_URL,
entities: [User, Post],
migrations: ['dist/db/migrations/*.js'],
migrationsTableName: 'migrations',
synchronize: false, // НИКОГДА true в production
logging: process.env.NODE_ENV === 'development' ? ['query', 'error'] : ['error'],
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
extra: {
max: 20,
idleTimeoutMillis: 30000,
}
})
// Инициализация
await AppDataSource.initialize()
Сущности
// db/entities/User.ts
import {
Entity, PrimaryGeneratedColumn, Column, CreateDateColumn,
UpdateDateColumn, OneToMany, Index, BeforeInsert, BeforeUpdate
} from 'typeorm'
import bcrypt from 'bcrypt'
import { Post } from './Post'
export enum UserRole {
USER = 'user',
MODERATOR = 'moderator',
ADMIN = 'admin',
}
@Entity('users')
@Index(['email'])
export class User {
@PrimaryGeneratedColumn('uuid')
id: string
@Column({ length: 255, unique: true })
email: string
@Column({ length: 255 })
name: string
@Column({ select: false }) // не включать в SELECT по умолчанию
passwordHash: string
@Column({ type: 'enum', enum: UserRole, default: UserRole.USER })
role: UserRole
@OneToMany(() => Post, (post) => post.author)
posts: Post[]
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date
@BeforeInsert()
@BeforeUpdate()
async hashPassword() {
if (this.passwordHash && !this.passwordHash.startsWith('$2b$')) {
this.passwordHash = await bcrypt.hash(this.passwordHash, 12)
}
}
}
// db/entities/Post.ts
import {
Entity, PrimaryGeneratedColumn, Column, CreateDateColumn,
UpdateDateColumn, ManyToOne, ManyToMany, JoinTable, JoinColumn, Index
} from 'typeorm'
import { User } from './User'
import { Tag } from './Tag'
@Entity('posts')
@Index(['authorId', 'createdAt'])
@Index(['published', 'createdAt'])
export class Post {
@PrimaryGeneratedColumn('uuid')
id: string
@Column({ type: 'text' })
title: string
@Column({ type: 'text', nullable: true })
content: string | null
@Column({ default: false })
published: boolean
@Column({ name: 'author_id' })
authorId: string
@ManyToOne(() => User, (user) => user.posts, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'author_id' })
author: User
@ManyToMany(() => Tag, (tag) => tag.posts, { cascade: true })
@JoinTable({
name: 'posts_to_tags',
joinColumn: { name: 'post_id' },
inverseJoinColumn: { name: 'tag_id' }
})
tags: Tag[]
@Column({ default: 0 })
viewCount: number
@Column({ type: 'timestamptz', nullable: true })
publishedAt: Date | null
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date
}
Миграции
# Генерация миграции из diff схемы
npx typeorm migration:generate -n AddUserProfile -d dist/db/data-source.js
# Создать пустую миграцию вручную
npx typeorm migration:create -n AddIndexes
# Применить
npx typeorm migration:run -d dist/db/data-source.js
# Откатить последнюю
npx typeorm migration:revert -d dist/db/data-source.js
// db/migrations/1234567890-AddUserProfile.ts
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddUserProfile1234567890 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE profiles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL UNIQUE REFERENCES users(id) ON DELETE CASCADE,
bio TEXT,
avatar_url VARCHAR(500),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
)
`)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE profiles`)
}
}
Query Builder
const postRepository = AppDataSource.getRepository(Post)
// Поиск с пагинацией
async function findPosts(opts: { page: number; limit: number; search?: string }) {
const { page, limit, search } = opts
const qb = postRepository.createQueryBuilder('post')
.leftJoinAndSelect('post.author', 'author')
.leftJoinAndSelect('post.tags', 'tag')
.where('post.published = :published', { published: true })
.orderBy('post.createdAt', 'DESC')
.skip((page - 1) * limit)
.take(limit)
if (search) {
qb.andWhere(
'post.title ILIKE :search OR post.content ILIKE :search',
{ search: `%${search}%` }
)
}
const [items, total] = await qb.getManyAndCount()
return { items, total, pages: Math.ceil(total / limit) }
}
// Сложные агрегаты
const stats = await AppDataSource.query(`
SELECT
date_trunc('week', created_at) AS week,
count(*) AS posts,
count(*) FILTER (WHERE published = true) AS published
FROM posts
WHERE created_at >= now() - interval '90 days'
GROUP BY 1
ORDER BY 1
`)
Subscribers (аналог хуков)
import { EntitySubscriberInterface, EventSubscriber, InsertEvent, UpdateEvent } from 'typeorm'
@EventSubscriber()
export class PostSubscriber implements EntitySubscriberInterface<Post> {
listenTo() { return Post }
async afterInsert(event: InsertEvent<Post>) {
await SearchIndexer.index('posts', event.entity)
}
async afterUpdate(event: UpdateEvent<Post>) {
if (event.updatedColumns.some(c => c.propertyName === 'published')) {
await NotificationService.notifyFollowers(event.entity)
}
}
}
NestJS интеграция
// app.module.ts
import { TypeOrmModule } from '@nestjs/typeorm'
@Module({
imports: [
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
type: 'postgres',
url: config.get('DATABASE_URL'),
entities: [__dirname + '/**/*.entity{.ts,.js}'],
migrations: [__dirname + '/db/migrations/*{.ts,.js}'],
migrationsRun: true,
synchronize: false,
})
}),
TypeOrmModule.forFeature([User, Post])
]
})
export class AppModule {}
Сроки
Базовая настройка TypeORM с сущностями, миграциями и репозиториями: 1–2 дня. Интеграция в NestJS-проект с модулями и тестами: 2–3 дня. Перевод существующего проекта с другого ORM на TypeORM: 3–5 дней.







