# NestJS และ TypeORM ในปี 2026: Migrations, Relations และคำถามสัมภาษณ์งาน > คู่มือ NestJS กับ TypeORM ฉบับสมบูรณ์: การตั้งค่า DataSource, การจัดการ migration, การออกแบบ entity relation แบบ OneToMany และ ManyToMany, repository pattern, transaction และคำถามสัมภาษณ์ที่พบบ่อยสำหรับ backend developer ในปี 2026 - Published: 2026-06-19 - Updated: 2026-06-19 - Author: SharpSkill - Reading time: 5 min --- 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 สามารถทำงานได้อย่างเป็นอิสระ ```typescript // src/config/typeorm.config.ts 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 ```typescript // src/app.module.ts 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('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 สะดวกยิ่งขึ้น ```json // package.json (scripts section) { "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` ```typescript // src/entities/user.entity.ts 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; } ``` ```typescript // src/entities/order.entity.ts 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 โดยอัตโนมัติ ```typescript // src/entities/product.entity.ts 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 สองตัวแทน ## Repository Pattern และ Feature Module NestJS ส่งเสริมการแบ่งโค้ดเป็นโมดูลย่อย (feature module) แต่ละโมดูลจะลงทะเบียน entity ผ่าน `TypeOrmModule.forFeature()` เพื่อเข้าถึง repository ที่เกี่ยวข้อง ```typescript // src/orders/orders.module.ts 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 ที่ต้องการความยืดหยุ่นมากกว่า ```typescript // src/orders/orders.service.ts 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, ) {} async findByUser(userId: string): Promise { return this.orderRepo.find({ where: { user: { id: userId } }, relations: ['user'], order: { createdAt: 'DESC' }, }); } async findWithFilters(status: string, minTotal: number): Promise { 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 ```typescript // src/orders/orders.service.ts (additional method) async createOrderWithItems( userId: string, items: { productId: string; quantity: number }[], dataSource: DataSource, ): Promise { 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 ได้หากเกิดข้อผิดพลาดในภายหลัง สิ่งนี้ทำให้เกิดข้อมูลที่ไม่สอดคล้องกันในฐานข้อมูล ## สรุป การใช้ 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 แต่ต้องอธิบายเหตุผลเบื้องหลังการตัดสินใจด้านสถาปัตยกรรมได้อย่างชัดเจน --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/node-nestjs/nestjs-typeorm-migrations-relations-best-practices