הגדרת TypeORM עבור יישום האינטרנט שלך: ישויות, מיגרציות, מאגרי נתונים
בעת פיתוח עם Node.js ו-TypeScript, רבים נתקלים באותה בעיה: כתיבת שאילתות SQL ידנית וסנכרון הסכמה עם הקוד. טעות אחת בשאילתת SQL — והפרודקשן קורס. TypeORM פותר בעיה זו, אך התצורה שלו דורשת התחשבות בעשרות ניואנסים: מהגדרות חיבור ועד יצירת מיגרציות. אנו נעזור לכם להגדיר TypeORM עבור הפרויקט שלכם — משרת פשוט ועד יישום NestJS מורכב. למהנדסים שלנו יש ניסיון של למעלה מ-10 שנים בעבודה עם מסדי נתונים רלציוניים והם מבטיחים יציבות של הסכמה שלכם.
מדוע הגדרת TypeORM סטנדרטית מובילה לעיתים קרובות לבעיות?
שאילתת N+1 — בעיה קלאסית בעת שימוש ב-ORM. ללא אופטימיזציה, טעינת 100 מאמרים עם מחברים תייצר 101 שאילתות. TypeORM מאפשר להגדיר בקלות טעינה מוקדמת באמצעות relations או להשתמש ב-Query Builder לשליטה מדויקת. כאב נוסף הוא synchronize: true בפרודקשן: זה מוביל לאובדן נתונים כאשר הסכמה משתנה. תצורת מיגרציות נכונה עם יצירת קובצי diff היא הדרך הבטוחה היחידה לעדכון מסד הנתונים. הבעיה השלישית היא ביצועים: שאילתות לא מותאמות יוצרות עומס נוסף. TypeORM מספק כלים לניטור וכיוונון עדין: subscribers למטמון, connection pooling, ורישום שאילתות איטיות. תחת עומס טיפוסי של 10,000 בקשות לדקה, תצורה נכונה מפחיתה את מספר השאילתות ב-60%.
כיצד אנו מגדירים את TypeORM: מקרה מבחן
מהניסיון שלנו: בפרויקט עם PostgreSQL ועומס של 10,000 בקשות לדקה, נתקלנו בירידה בביצועים עקב N+1 בעת טעינת תגים קשורים. פתרון: השתמשנו ב-Query Builder עם leftJoinAndSelect ופאג'ינציה באמצעות skip/take. בנוסף, הגדרנו connection pool עם max: 20 ו-idleTimeoutMillis: 30000, מה שהפחית את זמן התגובה ב-40%. שימוש ב-Query Builder אפשר לבצע שאילתה מורכבת פי 3 מהר יותר בהשוואה לטעינה דרך relations. connection pooling נכון חסך ללקוח כ-$1.4k–1.9k בשנה על תשתית. הזמינו הגדרת TypeORM — קבלו ייעוץ ממהנדס תוך שעה.
// 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()
מה כלול בהגדרת TypeORM?
תהליך העבודה מורכב מחמישה שלבים:
- ניתוח — אנו בוחנים את סכמת מסד הנתונים הנוכחית, העומס ופרופיל השאילתות.
- עיצוב — אנו בוחרים תבנית (Active Record או Data Mapper), מעצבים ישויות ויחסים.
- יישום — אנו מגדירים את החיבור, כותבים ישויות עם decorators, יוצרים מיגרציות ומאגרי נתונים.
- בדיקות — אנו בודקים ביצועים, היעדר N+1 ונכונות המיגרציות.
- פריסה וניטור — אנו פורסים לפרודקשן, מגדירים רישום והתראות.
מה כלול בתוצאה:
- תצורת DataSource עם pool ו-SSL (תומך בכל מסד נתונים: PostgreSQL, MySQL, SQLite).
- מאגרי נתונים וישויות עם אינדקסים ויחסים.
- מיגרציות (יצירה, הרצה, rollback).
- Query Builder לשאילתות טיפוסיות.
- אינטגרציית NestJS (מודול, שירותים, בקרים).
- Subscribers למטמון והתראות.
- תיעוד API וארכיטקטורה.
- אחריות ל-30 יום לאחר המסירה — אנו מתקנים באגים בחינם.
השוואת תבניות ובחירת ישויות
| מאפיין | Active Record | Data Mapper |
|---|---|---|
| הפרדת לוגיקה | נתונים והתנהגות יחד | נתונים בישות, התנהגות במאגרי נתונים |
| מורכבות | נמוכה, מתאים ל-CRUD פשוט | גבוהה, דורש יותר קוד |
| בדיקות | קשה יותר עקב קריאות ישירות למסד הנתונים | קל יותר בזכות מאגרי נתונים |
| גמישות | מוגבלת | גבוהה, קל לשנות שאילתות |
| פופולריות | בפרויקטים קטנים | בפתרונות ארגוניים גדולים |
המהנדסים שלנו תמיד ממליצים על Data Mapper עבור פרויקטים עם לוגיקה עסקית, מכיוון שהוא נותן יותר שליטה ומפשט בדיקות. אך אם אתם צריכים אב-טיפוס מהיר — Active Record הוא גם אפשרות עובדת. בחירת ישויות נכונה היא קריטית לביצועים. השתמשו ב-// 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() עבור מערכות מבוזרות, @PrimaryGeneratedColumn('uuid') עבור שדות המסוננים לעיתים קרובות. מאגר נתונים הוא שכבה בין הלוגיקה העסקית למסד הנתונים. עטפו את כל השאילתות לישות במאגר נתונים: זה הופך את הקוד לשימוש חוזר וניתן לבדיקה.
| Decorator | מטרה |
|---|---|
@Index() |
UUID או מספר שלם עם הגדלה אוטומטית |
@PrimaryGeneratedColumn |
שדה פשוט עם סוג ואפשרויות |
@Column |
אינדקס להאצת שאילתות |
@Index |
יחס של רבים-לאחד |
@ManyToOne |
הצד ההפוך של היחס |
דוגמה למאגר נתונים עם פאג'ינציה:
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
`) מדוע מיגרציות חשובות לפרודקשן?
לפי התיעוד הרשמי של TypeORM, מיגרציות הן הדרך הבטוחה היחידה לניהול הסכמה בפרודקשן. מיגרציות מאפשרות גרסת שינויים בסכמת מסד הנתונים והחלתם ברצף. TypeORM יכול ליצור מיגרציות אוטומטית בהתבסס על שינויים בישויות. זה חוסך עד 50% מזמן הפיתוח של מיגרציות בהשוואה לכתיבת SQL ידנית. דוגמה ליצירה והחלה:
# Генерация миграции из 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`)
}
} אינטגרציית NestJS ו-Subscribers לאירועים
ב-NestJS, הגדרת TypeORM מסתכמת בייבוא TypeOrmModule עם תצורה. בנוסף, ניתן להזריק מאגרי נתונים באמצעות ה-decorator @OneToMany. Subscribers מאפשרים להגיב לשינויים בישויות: לאחר insert, עדכון אינדקס חיפוש; לאחר פרסום, שליחת התראה. זהו כלי חזק אם לא משתמשים בו יותר מדי. דוגמה להגדרת 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 {} טעויות נפוצות וכיצד להימנע מהן
- Synchronize בפרודקשן — לעולם לא. השתמשו במיגרציות.
- חוסר אינדקסים — הוסיפו
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 `)על שדות שאתם מסננים לעיתים קרובות. - התעלמות מ-connection pool — הגדירו
# Генерация миграции из 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`) } }. - שאילתות N+1 — השתמשו ב-
@InjectRepositoryאו Query Builder עם// 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 {}. - אחסון סיסמאות בישות — השתמשו ב-
@Index()ו-hash ב-extra.max.
זמנים משוערים ועלות
הגדרת TypeORM בסיסית עם ישויות, מיגרציות ומאגרי נתונים: 1–2 ימים. אינטגרציה לפרויקט NestJS עם מודולים ובדיקות: 2–3 ימים. העברת פרויקט קיים מ-ORM אחר ל-TypeORM: 3–5 ימים. עלות תצורת TypeORM בסיסית תלויה במורכבות הפרויקט. צרו קשר להערכה חינמית — זה לוקח לא יותר משעה.







