Налаштування 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
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) }
}
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 днів.







