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

คู่มือ NestJS กับ TypeORM ฉบับสมบูรณ์: การตั้งค่า DataSource, การจัดการ migration, การออกแบบ entity relation แบบ OneToMany และ ManyToMany, repository pattern, transaction และคำถามสัมภาษณ์ที่พบบ่อยสำหรับ backend developer ในปี 2026

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

NestJS และ TypeORM เป็นหนึ่งในชุดเครื่องมือที่ได้รับความนิยมสูงสุดสำหรับการพัฒนา backend ด้วย Node.js ในปี 2026 NestJS นำเสนอสถาปัตยกรรมแบบ modular พร้อมระบบ dependency injection ที่แข็งแกร่ง ในขณะที่ TypeORM มอบความสามารถในการจัดการฐานข้อมูลแบบ type-safe ผ่าน decorator และ entity class อย่างไรก็ตาม ในทางปฏิบัติมักเกิดปัญหาและข้อสงสัยเกี่ยวกับการจัดการ migration, การออกแบบ relation ระหว่าง entity และการใช้งาน transaction อย่างถูกต้อง ซึ่งหัวข้อเหล่านี้ยังเป็นคำถามที่พบบ่อยในการสัมภาษณ์งานตำแหน่ง backend developer อีกด้วย

บทความนี้ครอบคลุมแนวคิดหลักของการใช้ TypeORM ร่วมกับ NestJS ตั้งแต่การตั้งค่า DataSource, การสร้างและรัน migration, การออกแบบ entity relation ประเภทต่าง ๆ ไปจนถึง repository pattern, transaction และคำถามสัมภาษณ์ที่ช่วยเสริมสร้างความเข้าใจเชิงลึก

TypeORM DataSource API

TypeORM ได้ปรับปรุง configuration API อย่างมีนัยสำคัญ โปรเจกต์ที่ยังใช้ ormconfig.json หรือ createConnection ควรย้ายมาใช้ DataSource API ตัวอย่างทั้งหมดในบทความนี้ใช้ API เวอร์ชันปัจจุบัน

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

ขั้นตอนแรกคือการสร้างไฟล์ DataSource configuration แยกต่างหาก ไฟล์นี้จะถูกใช้โดย TypeORM CLI สำหรับการรัน migration การแยกไฟล์ configuration ออกจาก NestJS context ทำให้ CLI สามารถทำงานได้อย่างเป็นอิสระ

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,
});

ค่า synchronize: false เป็นสิ่งที่ต้องให้ความสำคัญเป็นพิเศษ ในโหมดพัฒนาอาจดูสะดวกที่จะตั้ง synchronize: true เพื่อให้ TypeORM ปรับ schema ของฐานข้อมูลโดยอัตโนมัติ แต่ในสภาพแวดล้อม production การทำเช่นนั้นอาจนำไปสู่การสูญหายของข้อมูลและพฤติกรรมที่ไม่คาดคิด ดังนั้นจึงควรใช้ migration แทนเสมอ

ใน AppModule การเชื่อมต่อ TypeORM จะถูกตั้งค่าผ่าน asynchronous configuration โดยใช้ ConfigService เพื่ออ่านค่า environment variable

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,
        synchronize: false,
      }),
    }),
  ],
})
export class AppModule {}

ความแตกต่างระหว่าง config.get() กับ config.getOrThrow() มีความสำคัญในเชิงปฏิบัติ getOrThrow() จะ throw exception ทันทีเมื่อตัวแปรที่ต้องการไม่มีอยู่ สำหรับค่า configuration ที่สำคัญอย่างพอร์ตของฐานข้อมูล วิธีนี้ช่วยให้ตรวจพบปัญหาการตั้งค่าได้ตั้งแต่เริ่มต้นแอปพลิเคชัน แทนที่จะใช้ค่า default โดยไม่มีการแจ้งเตือน

Migration: การสร้าง รัน และย้อนกลับ

Migration เป็นหัวใจสำคัญของกลยุทธ์การจัดการฐานข้อมูลในระดับมืออาชีพ migration ทำหน้าที่บันทึกการเปลี่ยนแปลง schema แบบมีเวอร์ชันและสามารถทำซ้ำได้ npm script ต่อไปนี้ช่วยให้การทำงานกับ TypeORM CLI สะดวกยิ่งขึ้น

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"
}

ขั้นตอนการทำงานในทางปฏิบัติมีดังนี้ เริ่มจากการแก้ไข entity จากนั้นรันคำสั่ง npm run migration:generate --name=AddUserTable เพื่อสร้างไฟล์ migration โดยอัตโนมัติ ซึ่งจะเปรียบเทียบความแตกต่างระหว่าง schema ปัจจุบันกับ entity definition ที่แก้ไขใหม่ จากนั้นใช้ npm run migration:run เพื่อ apply migration และหากต้องการย้อนกลับ ให้ใช้ npm run migration:revert เพื่อยกเลิก migration ล่าสุด

ข้อผิดพลาดที่พบบ่อยในหลายโปรเจกต์คือการเขียน migration ด้วยมือ ทั้งที่คำสั่ง migration:generate สามารถเปรียบเทียบและสร้าง migration ให้โดยอัตโนมัติ การเขียน migration ด้วยมือจำเป็นเฉพาะกรณีที่ต้อง transform ข้อมูล เช่น การเปลี่ยนชื่อ column พร้อมย้ายข้อมูลเดิม

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

การเปลี่ยนชื่อ column จะถูกแปลงเป็นคำสั่ง DROP + ADD แทนที่จะเป็น ALTER RENAME ซึ่งจะทำให้ข้อมูลเดิมสูญหาย ควรตรวจสอบคำสั่ง SQL ในเมธอด up() ทุกครั้งและแก้ไขด้วยมือหากจำเป็น

Entity และ Relation: การออกแบบโครงสร้างข้อมูล

TypeORM ใช้ decorator ในการแมปโครงสร้างฐานข้อมูลลงใน TypeScript class โดยตรง relation ที่สำคัญสามประเภทได้แก่ OneToMany/ManyToOne, ManyToMany และ OneToOne

OneToMany และ ManyToOne

ความสัมพันธ์แบบคลาสสิกระหว่างผู้ใช้ (User) กับคำสั่งซื้อ (Order) แสดงให้เห็นการทำงานร่วมกันของ @OneToMany และ @ManyToOne

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[];

  @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' })
  user: User;
}

สิ่งสำคัญที่ต้องเข้าใจคือ @OneToMany ไม่ได้สร้าง column ในฐานข้อมูล foreign key จะอยู่ที่ฝั่ง @ManyToOne เสมอ @JoinColumn decorator กำหนดชื่อ column ของ foreign key อย่างชัดเจน ช่วยให้อ่าน schema ของฐานข้อมูลได้ง่ายและหลีกเลี่ยงปัญหาชื่อซ้ำ ตัวเลือก onDelete: 'CASCADE' ทำให้คำสั่งซื้อทั้งหมดถูกลบโดยอัตโนมัติเมื่อผู้ใช้ถูกลบออกจากระบบ

ManyToMany

สำหรับความสัมพันธ์ระหว่างสินค้า (Product) กับหมวดหมู่ (Category) ที่สินค้าหนึ่งรายการสามารถอยู่ในหลายหมวดหมู่ และหนึ่งหมวดหมู่สามารถมีหลายสินค้า จะใช้ ManyToMany relation โดย 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' })
  categories: Category[];
}

@JoinTable decorator ระบุว่าฝั่งนี้เป็นเจ้าของ (owning side) ของ relation มีเพียงฝั่งเดียวเท่านั้นที่สามารถใช้ decorator นี้ได้ หาก junction table ต้องการ column เพิ่มเติม เช่น ลำดับการจัดเรียงหรือจำนวน ให้สร้าง entity แยกที่มี @ManyToOne relation สองตัวแทน

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

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

Repository Pattern และ Feature Module

NestJS ส่งเสริมการแบ่งโค้ดเป็นโมดูลย่อย (feature module) แต่ละโมดูลจะลงทะเบียน entity ผ่าน TypeOrmModule.forFeature() เพื่อเข้าถึง repository ที่เกี่ยวข้อง

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 {}

ใน service จะใช้ @InjectRepository เพื่อ inject repository เข้ามา สำหรับ query ที่ไม่ซับซ้อนสามารถใช้ Repository API ได้โดยตรง ในขณะที่ QueryBuilder เหมาะสำหรับ query ที่ต้องการความยืดหยุ่นมากกว่า

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'],
      order: { createdAt: 'DESC' },
    });
  }

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

เมธอด findByUser แสดงวิธีการกรองข้อมูลผ่าน relation ด้วย nested where condition ส่วน relations array ควบคุมว่า relation ใดจะถูกโหลดมาพร้อมกัน หากไม่ระบุ TypeORM จะส่งกลับเฉพาะข้อมูลคำสั่งซื้อโดยไม่มีข้อมูลผู้ใช้

QueryBuilder ในเมธอด findWithFilters ให้การควบคุมที่มากกว่า leftJoinAndSelect โหลด relation และทำให้ข้อมูลพร้อมใช้งานในผลลัพธ์ ส่วน parameterized query ด้วย :status และ :minTotal ป้องกัน SQL injection ได้อย่างมีประสิทธิภาพ

Transaction: การรับประกันความสมบูรณ์ของข้อมูล

เมื่อหลาย database operation ต้องทำงานเป็นหน่วยเดียวกัน (atomic) transaction เป็นสิ่งที่ขาดไม่ได้ TypeORM มีเมธอด DataSource.transaction() สำหรับจัดการ transaction

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) => {
    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);
  });
}

ภายใน transaction ต้องใช้ manager สำหรับทุก operation เท่านั้น หากใช้ repository ปกติแทน operation เหล่านั้นจะทำงานนอก transaction และหากเกิดข้อผิดพลาดในขั้นตอนถัดไป ข้อมูลที่บันทึกไปแล้วจะไม่สามารถ rollback ได้ ส่งผลให้ข้อมูลไม่สอดคล้องกัน เมื่อเกิด error ภายใน callback ทั้ง transaction จะถูก rollback โดยอัตโนมัติ

QueryRunner กับ transaction()

DataSource.transaction() เหมาะสำหรับ multi-step operation ทั่วไป ส่วน QueryRunner ให้การควบคุม commit/rollback point และ savepoint แบบ manual ซึ่งมีประโยชน์สำหรับ partial rollback ในงาน batch processing

คำถามสัมภาษณ์ที่พบบ่อยเกี่ยวกับ NestJS และ TypeORM

การสัมภาษณ์ทางเทคนิคเกี่ยวกับ NestJS และ TypeORM มักครอบคลุมสามด้านหลัก ได้แก่ ความเข้าใจด้านสถาปัตยกรรม การแก้ปัญหาเชิงปฏิบัติ และการหลีกเลี่ยงข้อผิดพลาดที่พบบ่อย

ทำไม synchronize ต้องปิดใน production?

synchronize: true ทำให้ TypeORM ปรับ schema ของฐานข้อมูลให้ตรงกับ entity definition ทุกครั้งที่แอปพลิเคชันเริ่มทำงาน การทำเช่นนี้อาจลบ column, เปลี่ยน data type หรือลบ table โดยไม่มีบันทึกการเปลี่ยนแปลง ในสภาพแวดล้อม production จะนำไปสู่การสูญหายของข้อมูล migration เป็นทางเลือกที่ดีกว่าเพราะให้การเปลี่ยนแปลง schema แบบมีเวอร์ชัน ทำซ้ำได้ และสามารถย้อนกลับได้

migration:generate ต่างจาก migration:create อย่างไร?

คำสั่ง migration:generate เปรียบเทียบ entity definition ปัจจุบันกับ database schema แล้วสร้างคำสั่ง SQL ที่จำเป็นโดยอัตโนมัติ ส่วน migration:create สร้างไฟล์ migration เปล่าที่ต้องเขียน SQL เอง ซึ่งเหมาะสำหรับ data migration หรือ seed operation

Foreign key อยู่ฝั่งไหนใน OneToMany/ManyToOne?

Foreign key อยู่ที่ฝั่ง @ManyToOne เสมอ ฝั่ง @OneToMany ไม่สร้าง column ในฐานข้อมูล ทำหน้าที่เพียงแสดง inverse relation ใน TypeScript เท่านั้น

เมื่อไหร่ควรใช้ QueryBuilder แทน Repository API?

Repository API (find, findOne, save) เหมาะสำหรับ CRUD operation พื้นฐานและ query ที่มีเงื่อนไขตรงไปตรงมา QueryBuilder จำเป็นเมื่อต้องการ complex join, subquery, aggregate function หรือ query ที่ต้องสร้างแบบ dynamic

แก้ปัญหา circular dependency ระหว่าง module ได้อย่างไร?

NestJS มี forwardRef() สำหรับจัดการ circular dependency ระหว่าง module อย่างไรก็ตาม การต้องใช้ forwardRef() มักบ่งชี้ว่ามีปัญหาด้านสถาปัตยกรรม แนวทางที่ดีกว่าคือการแยก logic ที่ใช้ร่วมกันออกเป็น shared module แยกต่างหาก

หากใช้ repository ปกติภายใน transaction จะเกิดอะไรขึ้น?

Operation ที่ผ่าน repository ปกติจะทำงานนอก transaction หมายความว่าการเปลี่ยนแปลงเหล่านั้นจะถูก commit ทันทีและไม่สามารถ rollback ได้หากเกิดข้อผิดพลาดในภายหลัง สิ่งนี้ทำให้เกิดข้อมูลที่ไม่สอดคล้องกันในฐานข้อมูล

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

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

สรุป

การใช้ NestJS ร่วมกับ TypeORM เป็น ecosystem ที่สมบูรณ์สำหรับการพัฒนา backend แบบ type-safe ด้วย Node.js ประเด็นสำคัญที่ควรจดจำมีดังนี้

  • Migration ต้องใช้แทน synchronize ในทุกสภาพแวดล้อมที่ไม่ใช่การพัฒนาในเครื่อง เพื่อรับประกันการเปลี่ยนแปลง schema ที่ปลอดภัยและตรวจสอบได้
  • Relation ต้องเข้าใจอย่างชัดเจนว่า foreign key อยู่ฝั่งใด @ManyToOne เป็นเจ้าของ foreign key เสมอ
  • Transaction ต้องใช้ manager ที่ได้รับจาก callback เท่านั้น ห้ามใช้ repository ปกติภายใน transaction โดยเด็ดขาด
  • QueryBuilder เสริม Repository API สำหรับ query ที่ซับซ้อน โดยเฉพาะเมื่อต้องการ join, filter หรือ aggregate แบบ dynamic
  • สำหรับการสัมภาษณ์ทางเทคนิค ไม่ใช่เพียงแค่รู้ syntax แต่ต้องอธิบายเหตุผลเบื้องหลังการตัดสินใจด้านสถาปัตยกรรมได้อย่างชัดเจน

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

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

แชร์

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