NestJS และ TypeORM ในปี 2026: Migration, Relation และคำถามสัมภาษณ์

เชี่ยวชาญการผสานรวม NestJS TypeORM กับ TypeORM 1.1 migration, entity relation, repository pattern และคำถามสัมภาษณ์ทั่วไปสำหรับ backend developer

แผนภาพสถาปัตยกรรม migration และความสัมพันธ์ของ NestJS และ TypeORM

NestJS TypeORM ยังคงเป็นหนึ่งในการผสานรวม ORM ที่ผ่านการทดสอบมากที่สุดสำหรับ backend Node.js ระดับองค์กร ด้วย TypeORM เวอร์ชัน 1.0 ที่ออกในเดือนพฤษภาคม 2026 ตามด้วย patch series 1.1.x ที่แก้ไขการจัดการ connection และการบันทึก relation และ NestJS 12 ที่เสร็จสิ้นการย้ายไปยัง ESM stack นี้ได้พัฒนาเป็นพื้นฐานที่มั่นคงสำหรับแอปพลิเคชัน production คู่มือนี้ครอบคลุม pattern สำคัญที่ backend developer ทุกคนควรเชี่ยวชาญ: migration, entity relation, repository pattern และคำถามสัมภาษณ์ที่มักพบ

NestJS 12 และ TypeORM 1.1

NestJS 12 ส่งมอบ core package เป็น ESM ต้องการ Node.js 20.19+ หรือ 22.12+ แพ็คเกจ @nestjs/typeorm v12 ก็เช่นกัน TypeORM 1.1.2 แก้ไข connection leak ใน DataSource.initialize() และแก้ไขการบันทึก junction row ของ many-to-many เมื่อ relation ไม่ถูกโหลด

การตั้งค่า Migration TypeORM ใน NestJS 12

กฎข้อแรกของ database production: อย่าใช้ synchronize: true Auto-sync เปรียบเทียบ entity metadata กับ schema ที่ทำงานอยู่และทำการเปลี่ยนแปลงโดยตรง ซึ่งเสี่ยงต่อการสูญเสียข้อมูลเมื่อลบ column เปลี่ยน type หรือเปลี่ยนชื่อ field Migration ให้ทางเลือกที่มีเวอร์ชัน ตรวจสอบได้ และย้อนกลับได้

TypeORM 1.x ต้องการไฟล์ DataSource configuration แยกสำหรับ CLI แยกจาก module config ของ NestJS CLI ไม่สามารถ resolve dependency injection ของ NestJS ได้ จึงต้องการ entry point แบบ standalone

src/config/typeorm.config.tstypescript
import { DataSource } from 'typeorm';
import { config } from 'dotenv';

config(); // Load .env variables

export default new DataSource({
  type: 'postgres',
  host: process.env.DB_HOST,
  port: parseInt(process.env.DB_PORT || '5432'),
  username: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME,
  entities: ['src/**/*.entity.ts'],
  migrations: ['src/migrations/*.ts'],
  synchronize: false,
});

การตั้งค่า module NestJS สะท้อนการตั้งค่าเหล่านี้แต่ใช้ pattern TypeOrmModule.forRootAsync พร้อม dependency injection:

src/app.module.tstypescript
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    TypeOrmModule.forRootAsync({
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        type: 'postgres',
        host: config.get('DB_HOST'),
        port: config.getOrThrow<number>('DB_PORT'),
        username: config.get('DB_USER'),
        password: config.get('DB_PASSWORD'),
        database: config.get('DB_NAME'),
        autoLoadEntities: true, // Auto-register entities from feature modules
        synchronize: false,     // Always false in production
      }),
    }),
  ],
})
export class AppModule {}

autoLoadEntities: true แทนที่ array entities แบบ manual โดยลงทะเบียน entity ทุกตัวที่ import ผ่าน TypeOrmModule.forFeature() ใน feature module โดยอัตโนมัติ

ขั้นตอน Migration: Generate, Review, Run

TypeORM 1.x มาพร้อม typeorm-ts-node-commonjs เป็น CLI runner ที่แนะนำ แทนที่วิธีเก่า ts-node ./node_modules/typeorm/cli เพิ่ม script เหล่านี้ใน package.json:

package.json (scripts section)json
{
  "migration:generate": "typeorm-ts-node-commonjs migration:generate src/migrations/$npm_config_name -d src/config/typeorm.config.ts",
  "migration:run": "typeorm-ts-node-commonjs migration:run -d src/config/typeorm.config.ts",
  "migration:revert": "typeorm-ts-node-commonjs migration:revert -d src/config/typeorm.config.ts"
}

คำสั่ง generate เปรียบเทียบ entity metadata กับ database ที่ทำงานอยู่และสร้างคำสั่ง SQL ตรวจสอบ output ก่อน commit เสมอ TypeORM ตรวจจับการเพิ่ม column การลบ การเปลี่ยน type การแก้ไข index และการเปลี่ยนแปลง foreign key แต่ไม่ได้จับ data backfill การเปลี่ยนชื่อ table (ซึ่งปรากฏเป็น drop ตามด้วย create) และสิ่งที่เกี่ยวข้องกับ database extension หรือ trigger

ตรวจสอบทุก migration ที่สร้างขึ้น

Column ที่เปลี่ยนชื่อจะสร้าง DROP + ADD แทน ALTER RENAME ซึ่งจะทำลายข้อมูลที่มีอยู่ ตรวจสอบ SQL ใน method up() และปรับแก้ด้วยตนเองเมื่อจำเป็นเสมอ

Entity Relation: OneToMany, ManyToOne, ManyToMany

Entity relation ของ TypeORM แมปโดยตรงกับ SQL foreign key โดยใช้ decorator การเข้าใจ relation สามประเภทหลักและตัวเลือกของมันเป็นพื้นฐานสำหรับทุก module database NestJS

User ที่มีหลาย record Order แสดง pattern ที่พบบ่อยที่สุด:

src/entities/user.entity.tstypescript
import { Entity, PrimaryGeneratedColumn, Column, OneToMany, CreateDateColumn } from 'typeorm';
import { Order } from './order.entity';

@Entity('users')
export class User {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ unique: true })
  email: string;

  @Column()
  name: string;

  @OneToMany(() => Order, (order) => order.user)
  orders: Order[];  // No DB column created here; relation lives on the Order side

  @CreateDateColumn()
  createdAt: Date;
}
src/entities/order.entity.tstypescript
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
import { User } from './user.entity';

@Entity('orders')
export class Order {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column('decimal', { precision: 10, scale: 2 })
  total: number;

  @Column({ default: 'pending' })
  status: string;

  @ManyToOne(() => User, (user) => user.orders, { onDelete: 'CASCADE' })
  @JoinColumn({ name: 'user_id' })  // Explicit FK column name
  user: User;
}

ฝั่ง @ManyToOne เป็นเจ้าของ foreign key @JoinColumn เป็น optional บน @ManyToOne (TypeORM อนุมานได้) แต่การตั้งชื่ออย่างชัดเจนหลีกเลี่ยงความประหลาดใจเมื่อชื่อ column สำคัญสำหรับ raw query หรือ migration diff

สำหรับ relation many-to-many TypeORM สร้าง junction table โดยอัตโนมัติ:

src/entities/product.entity.tstypescript
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from 'typeorm';
import { Category } from './category.entity';

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

  @Column()
  name: string;

  @Column('decimal', { precision: 10, scale: 2 })
  price: number;

  @ManyToMany(() => Category, (category) => category.products)
  @JoinTable({ name: 'product_categories' })  // Owns the junction table
  categories: Category[];
}

@JoinTable ต้องปรากฏบนด้านเดียวของ relation มันกำหนดว่า entity ใดเป็นเจ้าของ junction table และควบคุมชื่อของมัน

พร้อมที่จะพิชิตการสัมภาษณ์ Node.js / NestJS แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

Repository Pattern และ Custom Repository

ระบบ module และ DI ของ NestJS ผสานรวม TypeORM repository ผ่าน TypeOrmModule.forFeature() การ inject Repository<T> ค่าเริ่มต้นครอบคลุมการดำเนินการ CRUD มาตรฐาน เมื่อ query ซับซ้อนขึ้น custom repository จะห่อหุ้ม logic นั้น

src/orders/orders.module.tstypescript
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Order } from '../entities/order.entity';
import { OrdersService } from './orders.service';
import { OrdersController } from './orders.controller';

@Module({
  imports: [TypeOrmModule.forFeature([Order])],
  providers: [OrdersService],
  controllers: [OrdersController],
})
export class OrdersModule {}
src/orders/orders.service.tstypescript
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Order } from '../entities/order.entity';

@Injectable()
export class OrdersService {
  constructor(
    @InjectRepository(Order)
    private readonly orderRepo: Repository<Order>,
  ) {}

  async findByUser(userId: string): Promise<Order[]> {
    return this.orderRepo.find({
      where: { user: { id: userId } },
      relations: ['user'],          // Eager-load the user relation
      order: { createdAt: 'DESC' }, // Most recent first
    });
  }

  async findWithFilters(status: string, minTotal: number): Promise<Order[]> {
    // QueryBuilder for complex queries
    return this.orderRepo
      .createQueryBuilder('order')
      .leftJoinAndSelect('order.user', 'user')
      .where('order.status = :status', { status })
      .andWhere('order.total >= :minTotal', { minTotal })
      .orderBy('order.total', 'DESC')
      .getMany();
  }
}

API find() จัดการ query ที่ตรงไปตรงมา QueryBuilder จัดการ join, subquery, aggregation และสิ่งที่ต้องการควบคุม SQL อย่างละเอียด การใช้ทั้งสองใน service เดียวกันถูกต้องสมบูรณ์: ใช้ find() สำหรับ lookup ง่าย ๆ และ QueryBuilder เมื่อ query ไม่สามารถแสดงผ่าน options API

การจัดการ Transaction สำหรับความสมบูรณ์ของข้อมูล

การดำเนินการที่แก้ไขหลาย table ต้องการ transaction TypeORM เสนอสองวิธี: method DataSource.transaction() และ API QueryRunner

src/orders/orders.service.ts (additional method)typescript
async createOrderWithItems(
  userId: string,
  items: { productId: string; quantity: number }[],
  dataSource: DataSource,
): Promise<Order> {
  return dataSource.transaction(async (manager) => {
    // All operations use the transactional manager
    const order = manager.create(Order, {
      user: { id: userId },
      total: 0,
      status: 'pending',
    });
    const savedOrder = await manager.save(order);

    let total = 0;
    for (const item of items) {
      const product = await manager.findOneByOrFail(Product, { id: item.productId });
      total += product.price * item.quantity;

      await manager.save(OrderItem, {
        order: savedOrder,
        product,
        quantity: item.quantity,
        unitPrice: product.price,
      });
    }

    savedOrder.total = total;
    return manager.save(savedOrder);
  });
}

Method transaction() แบบ callback-based commit อัตโนมัติเมื่อสำเร็จและ rollback เมื่อมี error ที่ถูก throw ทุกการดำเนินการ database ใน callback ต้องใช้ manager ที่ให้มาแทน repository ที่ inject มิฉะนั้นการดำเนินการเหล่านั้นจะทำงานนอก transaction

QueryRunner vs transaction()

ใช้ DataSource.transaction() สำหรับการดำเนินการหลายขั้นตอนที่ตรงไปตรงมา เปลี่ยนไปใช้ QueryRunner เมื่อต้องการควบคุม commit/rollback point หรือ savepoint ด้วยตนเอง เช่น partial rollback ใน batch processing

คำถามสัมภาษณ์ NestJS TypeORM ที่พบบ่อย

คำถามเหล่านี้ปรากฏบ่อยในการสัมภาษณ์ backend developer แต่ละคำตอบมุ่งเป้าไปที่ความลึกที่คาดหวังในการสนทนาทางเทคนิคระดับ senior

ทำไม synchronize: true ไม่ควรใช้ใน production?

Auto-sync ใช้การเปลี่ยนแปลง schema ที่ทำลายโดยไม่มีการยืนยัน การลบ column เปลี่ยน type หรือลบ index เกิดขึ้นทันทีกับข้อมูลที่ทำงานอยู่ Migration ให้ version control, code review และความสามารถในการ rollback การใช้ synchronize ที่ปลอดภัยเพียงอย่างเดียวคือการพัฒนา local กับข้อมูลที่ทิ้งได้

ความแตกต่างระหว่าง relation eager และ lazy คืออะไร?

Relation eager โหลดอัตโนมัติกับทุก query บน entity parent Relation lazy คืนค่า Promise และ execute query แยกเฉพาะเมื่อเข้าถึง Eager loading ทำให้เกิด overhead แบบ N+1 เมื่อไม่ต้องการข้อมูลที่เกี่ยวข้อง Lazy loading เลื่อนค่าใช้จ่ายแต่อาจ trigger query ที่ไม่คาดคิดลึกใน call stack ตัวเลือก relations อย่างชัดเจนใน find() หรือ leftJoinAndSelect ใน QueryBuilder ให้การควบคุมเต็มที่ว่าจะโหลดอะไรต่อ query

cascade ทำงานอย่างไรใน relation TypeORM?

การตั้ง cascade: true บน relation หมายความว่าการ save entity parent จะ persist entity child ที่ยังไม่ได้ save ด้วย cascade: ['insert', 'update'] จำกัดพฤติกรรมเฉพาะการดำเนินการที่ระบุ Cascade ทำงานที่ระดับ ORM ไม่ใช่ระดับ database สำหรับ cascading delete ระดับ database ใช้ onDelete: 'CASCADE' บน decorator @ManyToOne ซึ่งสร้าง constraint ON DELETE CASCADE ใน migration

เมื่อไหร่ควรใช้ custom repository แทน Repository<T> ค่าเริ่มต้น?

Custom repository ห่อหุ้ม logic query ที่ซับซ้อนที่ไม่ได้อยู่ใน service สัญญาณที่ควรแยก custom repository: chain QueryBuilder เดียวกันปรากฏในหลาย service, query เกี่ยวข้องกับหลาย join หรือ subquery, หรือ query ต้องการ raw SQL การเรียก find() ง่าย ๆ ไม่จำเป็นต้องมี custom repository

สำหรับคำถามสถาปัตยกรรม NestJS เพิ่มเติม ดูคู่มือฉบับสมบูรณ์ guards และ interceptors NestJS

พร้อมที่จะพิชิตการสัมภาษณ์ Node.js / NestJS แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

แหล่งข้อมูล

  • TypeORM 1.0.0 Release - ข้อกำหนด ECMAScript 2023, การลบ Connection, ค่าเริ่มต้น validation ที่เข้มงวดขึ้น
  • TypeORM 1.1.2 Release - แก้ไข connection leak ใน DataSource.initialize(), แก้ไข junction row many-to-many
  • NestJS 12 Release - การย้าย ESM, ข้อกำหนด Node.js 20.19+/22.12+
  • @nestjs/typeorm 12.0.0 - Native ES modules, ความเข้ากันได้ CommonJS ผ่าน Node require(esm)

ประเด็นสำคัญสำหรับแอป NestJS TypeORM Production

  • ใช้ ไฟล์ config DataSource แยกต่างหาก สำหรับ CLI TypeORM แยกจาก module configuration ของ NestJS
  • อย่าเปิดใช้งาน synchronize ใน production เด็ดขาด พึ่งพา migration ที่สร้างและตรวจสอบแล้วสำหรับทุกการเปลี่ยนแปลง schema
  • วาง @ManyToOne บน entity ที่เป็นเจ้าของ foreign key และใช้ชื่อ @JoinColumn อย่างชัดเจนเพื่อความชัดเจนใน raw query และ output migration
  • เลือกระหว่าง options API find() สำหรับ query ง่าย ๆ และ QueryBuilder สำหรับ join ที่ซับซ้อน, aggregation และ logic แบบมีเงื่อนไข
  • ห่อ mutation หลาย table ใน transaction โดยใช้ DataSource.transaction() และตรวจสอบให้แน่ใจว่าทุกการดำเนินการใน callback ใช้ manager ที่เป็น transactional
  • ด้วย TypeORM 1.1.x ที่ทำให้ API surface มั่นคงและ NestJS 12 ที่เสร็จสิ้น การย้าย ESM stack นี้ยังคงเป็นตัวเลือกที่แข็งแกร่งสำหรับ backend องค์กรที่ต้องการการเข้าถึง database ที่มีโครงสร้างและ type-safe

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

ชาเลนจ์ประจำวัน

คุณหาบั๊กใน Node.js / NestJS เจอไหม

โค้ดจริงหนึ่งชิ้น บั๊กที่ซ่อนอยู่หนึ่งจุด วันละหนึ่งครั้ง ลองได้โดยไม่ต้องมีบัญชี

Anthony Fillion-Maillet

เขียนโดย

Anthony Fillion-Maillet

ผู้ก่อตั้ง SharpSkill

เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่

อัปเดตเมื่อ 21 ตุลาคม 2569

แท็ก

#nestjs
#typeorm
#migrations
#database
#typescript
#backend

แชร์

บทความที่เกี่ยวข้อง