ถ้าคุณเคยทำงานกับ Database ในโปรเจกต์จริง คุณต้องเคยเจอปัญหาเหล่านี้ เช่น Schema ใน Development ไม่ตรงกับ Production หรือทีมเพิ่ม Column ใหม่แล้วลืมบอกคนอื่น หรือต้อง Rollback การเปลี่ยนแปลง Database แต่ไม่รู้ว่าเปลี่ยนอะไรไปบ้าง ปัญหาเหล่านี้แก้ได้ด้วย Database Migration
บทความนี้จะสอนทุกอย่างเกี่ยวกับ Database Migration ตั้งแต่แนวคิดพื้นฐาน เครื่องมือยอดนิยมในแต่ละภาษา ไปจนถึง Best Practices ที่ใช้ใน Production จริง รวมถึงเทคนิค Zero-downtime Migration ที่หลายคนมองข้าม
Database Migration คืออะไร?
Database Migration คือกระบวนการจัดการการเปลี่ยนแปลง Database Schema อย่างเป็นระบบ เปรียบเทียบง่ายๆ ก็เหมือน Git สำหรับ Database ถ้า Git ติดตามการเปลี่ยนแปลงของ Source Code แล้ว Migration ก็ติดตามการเปลี่ยนแปลงของ Database Structure
แต่ละ Migration คือไฟล์ที่บรรจุคำสั่ง SQL หรือ Code ที่อธิบายว่าต้องเปลี่ยนแปลง Database อย่างไร โดยทั่วไปจะมี 2 ส่วน คือ Up (ทำการเปลี่ยนแปลง) และ Down (ย้อนกลับการเปลี่ยนแปลง)
-- Migration: 001_create_users_table.sql
-- Up: สร้างตาราง users
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_users_email ON users(email);
-- Down: ลบตาราง users
DROP INDEX IF EXISTS idx_users_email;
DROP TABLE IF EXISTS users;
ทำไม Database Migration ถึงสำคัญ?
1. Version Control สำหรับ Database: เหมือนกับที่ Git ช่วยติดตามว่า Source Code เปลี่ยนอะไรบ้าง Migration ช่วยติดตามว่า Database Schema เปลี่ยนอะไรบ้าง ใครเปลี่ยน เมื่อไหร่ และทำไม ทำให้สามารถ Audit ได้ทุกเมื่อ
2. ทำงานเป็นทีมได้: เมื่อทีมมีหลายคนทำงานพร้อมกัน แต่ละคนอาจต้องแก้ไข Database Schema สำหรับ Feature ของตัวเอง Migration ช่วยให้ทุกคนสามารถเพิ่ม Schema Changes ของตัวเอง แล้วระบบจะรัน Migration ตามลำดับให้อัตโนมัติ
3. Reproducible Environment: ทุกคนในทีมสามารถสร้าง Database ที่เหมือนกันทุกประการได้ แค่รันชุด Migration ทั้งหมดจากศูนย์ ไม่ต้องส่ง SQL Dump ให้กัน ไม่ต้องถามกันว่า "ใครเพิ่ม Column นี้ตอนไหร่"
4. Safe Rollback: ถ้า Migration ไหนมีปัญหา สามารถ Rollback กลับไปเวอร์ชันก่อนหน้าได้อย่างปลอดภัย เพราะมี Down Migration ที่อธิบายวิธีย้อนกลับ
5. CI/CD Integration: Migration สามารถรันอัตโนมัติเป็นส่วนหนึ่งของ Deployment Pipeline ทำให้ Database Schema อัพเดทพร้อมกับ Application Code ทุกครั้ง
Migration File Naming Convention
การตั้งชื่อไฟล์ Migration เป็นสิ่งสำคัญมาก เพราะ Migration Tool ส่วนใหญ่จะรันไฟล์ตามลำดับชื่อ มี Convention หลักๆ 2 แบบ
แบบ Sequential Number
migrations/
├── 001_create_users_table.sql
├── 002_create_products_table.sql
├── 003_add_email_to_users.sql
├── 004_create_orders_table.sql
└── 005_add_index_on_orders_date.sql
แบบ Timestamp
migrations/
├── 20260101120000_create_users_table.sql
├── 20260115143000_create_products_table.sql
├── 20260201090000_add_email_to_users.sql
├── 20260208160000_create_orders_table.sql
└── 20260215110000_add_index_on_orders_date.sql
เครื่องมือ Migration แบ่งตามภาษา
Java: Flyway
Flyway เป็น Migration Tool ที่นิยมที่สุดในโลก Java รองรับ SQL-based migrations และ Java-based migrations ทำงานร่วมกับ Spring Boot ได้อย่างดีเยี่ยม
-- V1__Create_users_table.sql (Flyway naming: V + version + __ + description)
CREATE TABLE users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- V2__Create_products_table.sql
CREATE TABLE products (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(200) NOT NULL,
price DECIMAL(10, 2) NOT NULL,
stock INT DEFAULT 0,
category_id BIGINT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- V3__Add_phone_to_users.sql
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
# รัน Flyway
flyway migrate # รัน Pending migrations
flyway info # ดูสถานะ Migration
flyway validate # ตรวจสอบ Migration files
flyway repair # แก้ไข Migration metadata
flyway clean # ลบทุกอย่าง (อย่าใช้ใน Production!)
flyway baseline # ตั้ง Baseline สำหรับ Database ที่มีอยู่แล้ว
# application.yml (Spring Boot + Flyway)
spring:
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true
validate-on-migrate: true
datasource:
url: jdbc:postgresql://localhost:5432/mydb
username: postgres
password: secret
Java: Liquibase
Liquibase เป็นอีกทางเลือกที่นิยมสำหรับ Java รองรับการเขียน Migration ในรูปแบบ XML, YAML, JSON หรือ SQL ทำให้ยืดหยุ่นกว่า Flyway
# changelog.yaml (Liquibase)
databaseChangeLog:
- changeSet:
id: 1
author: somchai
changes:
- createTable:
tableName: users
columns:
- column:
name: id
type: BIGINT
autoIncrement: true
constraints:
primaryKey: true
- column:
name: email
type: VARCHAR(255)
constraints:
nullable: false
unique: true
- column:
name: name
type: VARCHAR(100)
rollback:
- dropTable:
tableName: users
- changeSet:
id: 2
author: somchai
changes:
- addColumn:
tableName: users
columns:
- column:
name: phone
type: VARCHAR(20)
rollback:
- dropColumn:
tableName: users
columnName: phone
Python: Alembic (with SQLAlchemy)
Alembic เป็น Migration Tool มาตรฐานของ Python ที่ทำงานคู่กับ SQLAlchemy ORM สามารถ Auto-generate Migration จาก Model Changes ได้
# ติดตั้ง
pip install alembic sqlalchemy
# เริ่มต้น
alembic init migrations
# สร้าง Migration อัตโนมัติจาก Model changes
alembic revision --autogenerate -m "create users table"
# รัน Migration
alembic upgrade head # อัพเดทไปเวอร์ชันล่าสุด
alembic upgrade +1 # อัพเดท 1 step
alembic downgrade -1 # ย้อนกลับ 1 step
alembic downgrade base # ย้อนกลับทั้งหมด
alembic history # ดูประวัติ
alembic current # ดูเวอร์ชันปัจจุบัน
# migrations/versions/20260408_create_users.py
"""create users table
Revision ID: a1b2c3d4e5f6
Revises:
Create Date: 2026-04-08 10:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
revision = 'a1b2c3d4e5f6'
down_revision = None
def upgrade():
op.create_table(
'users',
sa.Column('id', sa.Integer(), primary_key=True),
sa.Column('email', sa.String(255), nullable=False, unique=True),
sa.Column('name', sa.String(100), nullable=False),
sa.Column('password_hash', sa.String(255), nullable=False),
sa.Column('is_active', sa.Boolean(), default=True),
sa.Column('created_at', sa.DateTime(), server_default=sa.func.now()),
)
op.create_index('idx_users_email', 'users', ['email'])
def downgrade():
op.drop_index('idx_users_email', 'users')
op.drop_table('users')
JavaScript/TypeScript: Prisma Migrate
Prisma Migrate เป็นส่วนหนึ่งของ Prisma ORM ที่กำลังได้รับความนิยมอย่างมากในปี 2026 ใช้ Prisma Schema Language ในการ Define Schema แล้ว Generate Migration อัตโนมัติ
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String
posts Post[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([email])
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
createdAt DateTime @default(now())
@@index([authorId])
}
# Prisma Migrate Commands
npx prisma migrate dev --name create_users # สร้าง + รัน Migration (dev)
npx prisma migrate deploy # รัน Pending migrations (production)
npx prisma migrate reset # Reset database + รัน migrations ใหม่
npx prisma migrate status # ดูสถานะ Migration
npx prisma db push # Push schema โดยไม่สร้าง migration file
npx prisma generate # Generate Prisma Client
JavaScript/TypeScript: Drizzle Kit
Drizzle ORM เป็น ORM ที่กำลังมาแรงในปี 2026 โดดเด่นเรื่อง Type Safety ที่ดีเยี่ยมและไม่ Abstract SQL จนเกินไป Drizzle Kit เป็น Migration Tool ของมัน
// drizzle/schema.ts
import { pgTable, serial, varchar, boolean, timestamp, integer, text } from "drizzle-orm/pg-core";
export const users = pgTable("users", {
id: serial("id").primaryKey(),
email: varchar("email", { length: 255 }).notNull().unique(),
name: varchar("name", { length: 100 }).notNull(),
passwordHash: varchar("password_hash", { length: 255 }).notNull(),
isActive: boolean("is_active").default(true),
createdAt: timestamp("created_at").defaultNow(),
});
export const posts = pgTable("posts", {
id: serial("id").primaryKey(),
title: varchar("title", { length: 200 }).notNull(),
content: text("content"),
published: boolean("published").default(false),
authorId: integer("author_id").references(() => users.id),
createdAt: timestamp("created_at").defaultNow(),
});
# Drizzle Kit Commands
npx drizzle-kit generate # Generate migration จาก schema changes
npx drizzle-kit migrate # รัน migrations
npx drizzle-kit push # Push schema โดยตรง (dev)
npx drizzle-kit studio # เปิด Database GUI
npx drizzle-kit check # ตรวจสอบ schema consistency
JavaScript: Knex.js
Knex.js เป็น SQL Query Builder ที่มี Migration System ในตัว เหมาะสำหรับคนที่ต้องการเขียน Migration เป็น JavaScript/TypeScript โดยไม่ต้องใช้ ORM เต็มตัว
// migrations/20260408100000_create_users.ts
import type { Knex } from "knex";
export async function up(knex: Knex): Promise<void> {
await knex.schema.createTable("users", (table) => {
table.increments("id").primary();
table.string("email", 255).notNull().unique();
table.string("name", 100).notNull();
table.string("password_hash", 255).notNull();
table.boolean("is_active").defaultTo(true);
table.timestamps(true, true);
table.index(["email"]);
});
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists("users");
}
# Knex Migration Commands
npx knex migrate:make create_users # สร้างไฟล์ migration ใหม่
npx knex migrate:latest # รัน pending migrations
npx knex migrate:rollback # ย้อนกลับ batch ล่าสุด
npx knex migrate:status # ดูสถานะ
Go: golang-migrate
-- 000001_create_users.up.sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- 000001_create_users.down.sql
DROP TABLE IF EXISTS users;
# golang-migrate Commands
migrate -path ./migrations -database "postgres://user:pass@localhost/db?sslmode=disable" up
migrate -path ./migrations -database "..." down 1
migrate -path ./migrations -database "..." version
migrate -path ./migrations -database "..." force 1
Go: Atlas (Modern Alternative)
Atlas เป็น Migration Tool รุ่นใหม่จาก Ariga ที่ใช้ Declarative Approach แทน Imperative คุณแค่ Define Schema ที่ต้องการ แล้ว Atlas จะคำนวณ Migration ให้อัตโนมัติ
# schema.hcl (Atlas Schema Language)
table "users" {
schema = schema.public
column "id" {
type = bigserial
}
column "email" {
type = varchar(255)
null = false
}
column "name" {
type = varchar(100)
null = false
}
primary_key {
columns = [column.id]
}
index "idx_users_email" {
columns = [column.email]
unique = true
}
}
# Atlas Commands
atlas schema inspect -u "postgres://..." # Inspect current schema
atlas schema diff --from file://schema.hcl --to "postgres://..." # Generate diff
atlas migrate diff create_users --to file://schema.hcl # Create migration
atlas migrate apply -u "postgres://..." # Apply migrations
เปรียบเทียบ Migration Tools
| Tool | ภาษา | รูปแบบ | Auto-generate | Rollback | จุดเด่น |
|---|---|---|---|---|---|
| Flyway | Java | SQL / Java | ไม่มี | Pro only | Simple, ใช้ง่าย |
| Liquibase | Java | XML/YAML/SQL | ได้ | ได้ | ยืดหยุ่นสูง |
| Alembic | Python | Python | ได้ | ได้ | SQLAlchemy integration |
| Prisma Migrate | JS/TS | Prisma Schema | ได้ | ได้ | Type-safe, DX ดี |
| Drizzle Kit | JS/TS | TypeScript | ได้ | ได้ | SQL-like, lightweight |
| Knex.js | JS/TS | JS/TS | ไม่มี | ได้ | Query builder + migration |
| golang-migrate | Go | SQL | ไม่มี | ได้ | CLI + Library |
| Atlas | Go | HCL / SQL | ได้ | ได้ | Declarative, modern |
Up/Down Migration Pattern
Migration ส่วนใหญ่ใช้ Pattern Up/Down (หรือ Forward/Backward) โดย Up คือคำสั่งที่ทำการเปลี่ยนแปลง และ Down คือคำสั่งที่ย้อนกลับ
ตัวอย่าง Up/Down ที่ดี
-- Up: เพิ่ม Column ใหม่
ALTER TABLE users ADD COLUMN avatar_url VARCHAR(500);
-- Down: ลบ Column ที่เพิ่ม
ALTER TABLE users DROP COLUMN avatar_url;
-- Up: เปลี่ยนชื่อ Column
ALTER TABLE users RENAME COLUMN name TO full_name;
-- Down: เปลี่ยนชื่อกลับ
ALTER TABLE users RENAME COLUMN full_name TO name;
-- Up: เพิ่ม Index
CREATE INDEX idx_orders_user_date ON orders(user_id, created_at);
-- Down: ลบ Index
DROP INDEX idx_orders_user_date;
กรณีที่ Down Migration เป็นไปไม่ได้
บาง Migration ไม่สามารถ Rollback ได้ เช่น การลบ Column ที่มีข้อมูลอยู่ ถ้า Rollback ข้อมูลก็หายไปแล้ว หรือการเปลี่ยน Data Type ที่ทำให้ข้อมูลเสียหาย ในกรณีนี้ ควรเขียน Down Migration ที่ throw error เพื่อป้องกันการ Rollback โดยไม่ตั้งใจ
# Python/Alembic — Migration ที่ Rollback ไม่ได้
def upgrade():
op.drop_column('users', 'legacy_field')
def downgrade():
raise Exception("Cannot rollback: data in 'legacy_field' has been permanently deleted")
Migration Best Practices
1. Small, Focused Changes
แต่ละ Migration ควรทำสิ่งเดียว อย่ารวมหลายเรื่องในไฟล์เดียว เช่น อย่าสร้าง 5 ตารางในไฟล์เดียว แยกเป็น 5 ไฟล์ ทำให้ Debug ง่ายกว่า Rollback ง่ายกว่า และ Review ง่ายกว่า
# แบบไม่ดี — ยัดทุกอย่างในไฟล์เดียว
001_create_everything.sql
# แบบดี — แยกแต่ละเรื่อง
001_create_users_table.sql
002_create_products_table.sql
003_create_orders_table.sql
004_add_index_on_users_email.sql
2. Backward Compatible Changes
Migration ที่ดีควร Backward Compatible หมายความว่า Application เวอร์ชันเก่าควรยังทำงานได้กับ Schema ใหม่ สิ่งนี้สำคัญมากสำหรับ Zero-downtime Deployment
-- แบบดี: เพิ่ม Column ใหม่ที่มี Default value
ALTER TABLE users ADD COLUMN phone VARCHAR(20) DEFAULT NULL;
-- Code เก่าไม่สนใจ Column ใหม่ — ยังทำงานได้
-- แบบไม่ดี: เปลี่ยนชื่อ Column ทันที
ALTER TABLE users RENAME COLUMN name TO full_name;
-- Code เก่าที่ใช้ Column "name" จะพังทันที!
3. Never Modify Applied Migrations
อย่าแก้ไข Migration ที่รันไปแล้วเด็ดขาด ถ้าต้องการเปลี่ยนแปลง ให้สร้าง Migration ใหม่ เพราะ Migration ที่รันแล้วถูกบันทึก Checksum ไว้ ถ้าแก้ไข Tool จะแจ้ง Error
4. Test Migrations
ทดสอบ Migration ทุกครั้งก่อน Deploy โดยเฉพาะ Down Migration ให้ทดสอบ Up แล้ว Down แล้ว Up อีกครั้ง เพื่อให้แน่ใจว่าทำงานได้ทั้งสองทิศทาง
# ทดสอบ Migration Cycle
alembic upgrade head # Up ทั้งหมด
alembic downgrade base # Down ทั้งหมด
alembic upgrade head # Up อีกครั้ง — ต้องผ่าน!
5. Data Migration แยกจาก Schema Migration
แยก Schema Migration (เปลี่ยนโครงสร้าง) ออกจาก Data Migration (เปลี่ยนข้อมูล) เพราะ Data Migration มักจะช้าและซับซ้อนกว่า
# แบบดี — แยก Schema กับ Data
003_add_full_name_column.sql # Schema: เพิ่ม Column
004_populate_full_name.sql # Data: คัดลอกข้อมูล
005_drop_first_last_name.sql # Schema: ลบ Column เก่า
# แบบไม่ดี — รวมกัน
003_merge_name_columns.sql # ทั้ง Schema + Data ในไฟล์เดียว
Zero-Downtime Migration Strategies
สำหรับ Production System ที่ต้องทำงาน 24/7 การเปลี่ยนแปลง Database Schema โดยไม่ทำให้ Service หยุดชะงัก เป็นเรื่องท้าทายมาก มีหลายเทคนิคที่ใช้กัน
Expand-Contract Pattern
เป็นเทคนิคที่นิยมที่สุดสำหรับ Zero-downtime Migration แบ่งการเปลี่ยนแปลงออกเป็น 3 ขั้นตอน
ขั้นตอนที่ 1: Expand (เพิ่มสิ่งใหม่)
-- เพิ่ม Column ใหม่ ยังไม่ลบของเก่า
ALTER TABLE users ADD COLUMN full_name VARCHAR(200);
-- Application Code: เขียนข้อมูลลงทั้ง name และ full_name
-- (Dual Write)
ขั้นตอนที่ 2: Migrate Data
-- คัดลอกข้อมูลจาก Column เก่าไป Column ใหม่
UPDATE users SET full_name = name WHERE full_name IS NULL;
-- Application Code: เปลี่ยนมาอ่านจาก full_name แทน name
-- (ยังคง Dual Write)
ขั้นตอนที่ 3: Contract (ลบของเก่า)
-- Application Code: หยุดเขียน name Column
-- รอจนมั่นใจว่า Code ใหม่ Deploy ครบทุก Instance
-- ลบ Column เก่า
ALTER TABLE users DROP COLUMN name;
Blue-Green for Database
สำหรับ Database ที่ต้อง Migrate ขนาดใหญ่ ใช้เทคนิค Blue-Green โดยสร้าง Database Copy (Green) ทำ Migration บน Green แล้ว Switch Traffic มาที่ Green เมื่อพร้อม
ข้อจำกัดคือต้องจัดการ Data Sync ระหว่าง Blue กับ Green ในช่วงเปลี่ยนผ่าน ซึ่งซับซ้อนมาก ใน PostgreSQL สามารถใช้ Logical Replication ช่วยได้
Handling Long-Running Migrations
Migration บางอย่างใช้เวลานานมาก เช่น การเพิ่ม Index บนตารางที่มีข้อมูลหลายล้าน Row หรือการ ALTER TABLE ที่ต้อง Rewrite ทั้งตาราง มีเทคนิคจัดการดังนี้
PostgreSQL: CREATE INDEX CONCURRENTLY
-- แบบปกติ — Lock ตาราง ไม่สามารถ Write ได้
CREATE INDEX idx_orders_date ON orders(created_at);
-- แบบ Concurrent — ไม่ Lock ตาราง แต่ใช้เวลานานกว่า
CREATE INDEX CONCURRENTLY idx_orders_date ON orders(created_at);
-- หมายเหตุ: CONCURRENTLY ไม่สามารถรันภายใน Transaction ได้
-- ต้องตั้งค่า Migration Tool ให้ไม่ Wrap ใน Transaction
MySQL: Online DDL
-- MySQL 8.0+ รองรับ Online DDL ที่ไม่ Lock ตาราง
ALTER TABLE orders ADD INDEX idx_date(created_at), ALGORITHM=INPLACE, LOCK=NONE;
-- เพิ่ม Column
ALTER TABLE users ADD COLUMN avatar VARCHAR(500), ALGORITHM=INSTANT;
Batch Data Migration
# Python — Migrate ข้อมูลแบบ Batch เพื่อไม่ให้ Lock นาน
def upgrade():
conn = op.get_bind()
batch_size = 1000
offset = 0
while True:
result = conn.execute(
text(f"SELECT id FROM users WHERE full_name IS NULL LIMIT {batch_size} OFFSET {offset}")
)
rows = result.fetchall()
if not rows:
break
ids = [row[0] for row in rows]
conn.execute(
text("UPDATE users SET full_name = name WHERE id = ANY(:ids)"),
{"ids": ids}
)
offset += batch_size
print(f"Migrated {offset} rows...")
Migration in CI/CD Pipeline
การรัน Migration อัตโนมัติเป็นส่วนหนึ่งของ Deployment Pipeline เป็นสิ่งจำเป็นสำหรับทีมที่ Deploy บ่อย
# GitHub Actions — Migration Pipeline
name: Deploy with Migration
on:
push:
branches: [main]
jobs:
migrate-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Migration on Staging
run: |
npx prisma migrate deploy
env:
DATABASE_URL: ${{ secrets.STAGING_DATABASE_URL }}
- name: Run Tests Against Migrated DB
run: npm test
- name: Run Migration on Production
run: |
npx prisma migrate deploy
env:
DATABASE_URL: ${{ secrets.PRODUCTION_DATABASE_URL }}
- name: Deploy Application
run: |
# Deploy app after migration succeeds
./deploy.sh
Multi-Tenant Migration
สำหรับ Application ที่มีหลาย Tenant (ลูกค้าหลายราย) การจัดการ Migration ซับซ้อนขึ้น มี 2 แนวทางหลัก
Shared Database — แยก Schema
-- สร้าง Schema สำหรับแต่ละ Tenant
CREATE SCHEMA tenant_001;
CREATE SCHEMA tenant_002;
-- Migration ต้องรันในทุก Schema
DO $$
DECLARE
tenant_schema TEXT;
BEGIN
FOR tenant_schema IN
SELECT schema_name FROM information_schema.schemata
WHERE schema_name LIKE 'tenant_%'
LOOP
EXECUTE format('ALTER TABLE %I.users ADD COLUMN avatar VARCHAR(500)', tenant_schema);
END LOOP;
END $$;
Database Per Tenant
# Python — รัน Migration ทุก Tenant Database
import asyncio
from alembic import command
from alembic.config import Config
async def migrate_all_tenants():
tenants = await get_all_tenant_configs()
for tenant in tenants:
alembic_cfg = Config("alembic.ini")
alembic_cfg.set_main_option("sqlalchemy.url", tenant.database_url)
try:
command.upgrade(alembic_cfg, "head")
print(f"Migrated tenant {tenant.name} successfully")
except Exception as e:
print(f"Failed to migrate tenant {tenant.name}: {e}")
# อย่าหยุด — ลอง Tenant ถัดไป
Schema Drift Detection
Schema Drift คือสถานการณ์ที่ Database Schema จริงไม่ตรงกับที่ Migration กำหนดไว้ เกิดได้จากการแก้ไข Schema ด้วยมือ โดยไม่ผ่าน Migration Tool
# Prisma — ตรวจสอบ Schema Drift
npx prisma migrate diff --from-schema-datasource prisma/schema.prisma --to-migrations prisma/migrations --exit-code
# Atlas — ตรวจสอบ Drift
atlas schema diff --from "postgres://..." --to file://schema.hcl
# ถ้ามี Drift จะแสดง SQL ที่ต้องรันเพื่อแก้ไข
Seeding Data
การ Seed Data คือการเพิ่มข้อมูลเริ่มต้นเข้า Database เช่น ข้อมูล Admin User, Category ต่างๆ, Country list เป็นต้น
// prisma/seed.ts — Prisma Seeding
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
async function main() {
// สร้าง Admin User
await prisma.user.upsert({
where: { email: "admin@example.com" },
update: {},
create: {
email: "admin@example.com",
name: "Admin",
role: "ADMIN",
},
});
// สร้าง Categories
const categories = ["Technology", "Science", "Business", "Health"];
for (const name of categories) {
await prisma.category.upsert({
where: { name },
update: {},
create: { name },
});
}
console.log("Seeding completed");
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect());
# รัน Seed
npx prisma db seed
Common Mistakes
1. Destructive Changes โดยไม่มี Data Backup
-- อย่าทำแบบนี้โดยไม่ Backup!
DROP TABLE users;
ALTER TABLE orders DROP COLUMN customer_email;
TRUNCATE TABLE logs;
2. Long-Running Locks
-- อันตราย: Lock ตารางใหญ่
ALTER TABLE orders ADD COLUMN total DECIMAL(10,2) NOT NULL DEFAULT 0;
-- NOT NULL + DEFAULT ทำให้ต้อง Rewrite ทั้งตาราง ใน PostgreSQL < 11
-- ปลอดภัยกว่า: เพิ่ม Column ที่ Nullable ก่อน
ALTER TABLE orders ADD COLUMN total DECIMAL(10,2);
-- แล้ว Update ข้อมูลแบบ Batch
-- แล้วค่อย SET NOT NULL
3. ไม่ Test Down Migration
หลายทีมเขียน Down Migration แต่ไม่เคยทดสอบ พอต้อง Rollback จริงๆ กลับพบว่า Down Migration ไม่ทำงาน ทำให้ต้องแก้ไขด้วยมือ
4. Running Migrations at Application Startup
// อย่าทำแบบนี้ใน Production!
// app.ts
async function bootstrap() {
await runMigrations(); // ถ้า Migration ช้า App จะ Start ไม่ขึ้น
await startServer();
}
// ควรรัน Migration แยกจาก Application Startup
// ใน CI/CD Pipeline หรือ Init Container (K8s)
Migration สำหรับ MongoDB
แม้ MongoDB จะเป็น Schemaless แต่ในทางปฏิบัติยังต้องจัดการ Schema Changes เช่น เปลี่ยนโครงสร้าง Document, เพิ่ม Index, เปลี่ยน Data Format
// MongoDB Migration ด้วย migrate-mongo
// migrations/20260408-add-email-index.js
module.exports = {
async up(db) {
await db.collection("users").createIndex(
{ email: 1 },
{ unique: true }
);
// Rename field
await db.collection("users").updateMany(
{ name: { $exists: true } },
{ $rename: { name: "fullName" } }
);
},
async down(db) {
await db.collection("users").dropIndex("email_1");
await db.collection("users").updateMany(
{ fullName: { $exists: true } },
{ $rename: { fullName: "name" } }
);
}
};
Database Versioning
Migration Tool ส่วนใหญ่จะสร้างตาราง Metadata เพื่อติดตามว่า Migration ไหนรันไปแล้ว
-- ตาราง Migration Metadata (ชื่อแตกต่างตาม Tool)
-- Flyway: flyway_schema_history
-- Alembic: alembic_version
-- Prisma: _prisma_migrations
-- Knex: knex_migrations
-- ตัวอย่าง Flyway schema_history
SELECT installed_rank, version, description, type, checksum, installed_on, success
FROM flyway_schema_history;
-- ตัวอย่าง Prisma _prisma_migrations
SELECT id, migration_name, started_at, finished_at, applied_steps_count
FROM _prisma_migrations;
สรุป
Database Migration เป็นทักษะที่ Backend Developer ทุกคนต้องเชี่ยวชาญ ไม่ว่าจะใช้ภาษาหรือ Framework อะไร การจัดการ Schema Changes อย่างเป็นระบบจะช่วยให้ทีมทำงานร่วมกันได้ดีขึ้น Deploy ได้มั่นใจขึ้น และลดโอกาสเกิดปัญหาใน Production
สิ่งสำคัญที่ต้องจำคือ ทุก Schema Change ต้องผ่าน Migration เสมอ อย่าแก้ Production Database ด้วยมือ ทำ Migration ให้เล็กและ Backward Compatible เสมอ ทดสอบทั้ง Up และ Down Migration ก่อน Deploy และรัน Migration แยกจาก Application Startup ใน CI/CD Pipeline
เลือก Migration Tool ที่เหมาะกับภาษาและ Ecosystem ของคุณ ถ้าใช้ Python ก็ Alembic ถ้าใช้ TypeScript ก็ Prisma Migrate หรือ Drizzle Kit ถ้าใช้ Java ก็ Flyway หรือ Liquibase เครื่องมือเหล่านี้จะช่วยให้คุณจัดการ Database ได้อย่างมืออาชีพ
