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

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 ส่งมอบ 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
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:
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:
{
"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
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 ที่พบบ่อยที่สุด:
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;
}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 โดยอัตโนมัติ:
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 นั้น
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 {}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
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
ใช้ 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ผู้ก่อตั้ง SharpSkill
เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่
อัปเดตเมื่อ 21 ตุลาคม 2569
แท็ก
แชร์
บทความที่เกี่ยวข้อง

NestJS + Prisma: สแตกแบ็กเอนด์สมัยใหม่สำหรับ Node.js
คู่มือฉบับสมบูรณ์ในการสร้าง API แบ็กเอนด์สมัยใหม่ด้วย NestJS และ Prisma ครอบคลุมการตั้งค่า โมเดล เซอร์วิส ทรานแซกชัน และแนวปฏิบัติที่ดี

NestJS: สร้าง REST API ที่สมบูรณ์ตั้งแต่เริ่มต้น
คู่มือฉบับสมบูรณ์สำหรับการสร้าง REST API ระดับมืออาชีพด้วย NestJS ครอบคลุม Controller, Service, Module, การตรวจสอบข้อมูลด้วย class-validator และการจัดการข้อผิดพลาด

NestJS และ MongoDB ในปี 2026: Mongoose, Aggregation และคำถามสัมภาษณ์
เชี่ยวชาญ NestJS กับ MongoDB และ Mongoose 9 เรียนรู้การออกแบบ schema, aggregation pipeline และเตรียมตัวสัมภาษณ์เทคนิคด้วยตัวอย่างจริง