บทที่ 2: MongoDB และ Mongoose Models
สารบัญเนื้อหา
บทที่ 2: MongoDB และ Mongoose Models
🎯 เป้าหมายการเรียนรู้
หลังจากเรียนบทนี้แล้ว คุณจะสามารถ:
- เข้าใจความแตกต่างระหว่าง SQL และ NoSQL Database
- เชื่อมต่อ MongoDB ด้วย Mongoose
- สร้าง Schema และ Model สำหรับจัดเก็บข้อมูล
- ทำ Data Validation และ Type Checking
- เข้าใจ Relationships ระหว่าง Collections
📚 ทฤษฎีและแนวคิด
MongoDB คืออะไร?
MongoDB คือ NoSQL Database ประเภท Document-based ที่เก็บข้อมูลในรูปแบบ JSON-like (BSON)
ความแตกต่างระหว่าง SQL และ NoSQL:
| SQL (MySQL, PostgreSQL) | NoSQL (MongoDB) |
|---|---|
| ตาราง (Tables) | คอลเลกชัน (Collections) |
| แถว (Rows) | เอกสาร (Documents) |
| คอลัมน์ (Columns) | ฟิลด์ (Fields) |
| Schema แบบตายตัว | Schema ยืดหยุ่น |
| JOIN ข้ามตาราง | Embedded Documents |
ตัวอย่างข้อมูลใน MongoDB:
{
"_id": "507f1f77bcf86cd799439011",
"email": "user@example.com",
"displayName": "John Doe",
"role": "user",
"createdAt": "2026-01-20T10:00:00.000Z"
}
Mongoose คืออะไร?
Mongoose คือ ODM (Object Data Modeling) Library สำหรับ MongoDB ใน Node.js
ฟีเจอร์หลัก:
- 📋 Schema Definition: กำหนดโครงสร้างข้อมูล
- ✅ Validation: ตรวจสอบความถูกต้องของข้อมูล
- 🔗 Relationships: จัดการความสัมพันธ์ระหว่าง Collections
- 🎣 Middleware/Hooks: ทำงานก่อน/หลัง save, update, delete
- 🔍 Query Builder: สร้าง queries ได้ง่าย
💻 โค้ดจริงจาก QUOM App
1. การเชื่อมต่อ MongoDB
ไฟล์: backend/src/config/db.js
import mongoose from "mongoose";
export async function connectDB() {
const uri = process.env.MONGO_URI;
if (!uri) throw new Error("Missing MONGO_URI");
await mongoose.connect(uri);
console.log("✅ [db] connected");
}
อธิบายทีละบรรทัด:
-
import mongoose from "mongoose"- นำเข้า Mongoose library
-
export async function connectDB()- สร้างฟังก์ชัน async เพื่อเชื่อมต่อ Database
asyncหมายความว่าฟังก์ชันนี้ทำงานแบบ asynchronous
-
const uri = process.env.MONGO_URI- อ่าน MongoDB connection string จาก Environment Variable
- ตัวอย่าง URI:
mongodb://localhost:27017/quom mongodb+srv://user:pass@cluster.mongodb.net/quom
-
if (!uri) throw new Error("Missing MONGO_URI")- ตรวจสอบว่ามี URI หรือไม่
- ถ้าไม่มี ให้ throw Error เพื่อหยุดการทำงาน
-
await mongoose.connect(uri)- เชื่อมต่อกับ MongoDB
awaitหมายความว่ารอให้เชื่อมต่อเสร็จก่อนทำงานต่อ
-
console.log("✅ [db] connected")- แสดงข้อความเมื่อเชื่อมต่อสำเร็จ
2. User Model (โมเดลผู้ใช้)
ไฟล์: backend/src/models/User.js
import mongoose from "mongoose";
const UserSchema = new mongoose.Schema(
{
email: { type: String, unique: true, required: true, index: true },
passwordHash: { type: String, required: true },
displayName: { type: String, default: "" },
role: { type: String, enum: ["user", "admin"], default: "user" },
preferences: {
notifications: {
push: { type: Boolean, default: true },
email: { type: Boolean, default: true },
reminders: { type: Boolean, default: true }
},
theme: { type: String, enum: ['light', 'dark', 'system'], default: 'system' },
language: { type: String, default: 'en' }
},
profile: {
bio: { type: String, default: '' },
avatar: { type: String, default: '' },
phone: { type: String, default: '' }
}
},
{ timestamps: true }
);
export const User = mongoose.model("User", UserSchema);
อธิบายโครงสร้าง Schema:
Field Types และ Options
-
email
email: { type: String, unique: true, required: true, index: true }type: String: ชนิดข้อมูลเป็น Stringunique: true: ห้ามซ้ำ (แต่ละ email ต้องไม่ซ้ำกัน)required: true: จำเป็นต้องมี (ห้ามเป็น null)index: true: สร้าง index เพื่อค้นหาเร็วขึ้น
-
passwordHash
passwordHash: { type: String, required: true }- เก็บรหัสผ่านที่ hash แล้ว (ไม่เก็บ plain text)
- จำเป็นต้องมี
-
displayName
displayName: { type: String, default: "" }default: "": ถ้าไม่ระบุ จะใช้ค่า default เป็น empty string
-
role (Enum)
role: { type: String, enum: ["user", "admin"], default: "user" }enum: ["user", "admin"]: จำกัดค่าที่เป็นไปได้เฉพาะ "user" หรือ "admin"- ถ้าใส่ค่าอื่นจะ error
Nested Objects (Embedded Documents)
-
preferences (การตั้งค่า)
preferences: { notifications: { push: { type: Boolean, default: true }, email: { type: Boolean, default: true }, reminders: { type: Boolean, default: true } }, theme: { type: String, enum: ['light', 'dark', 'system'], default: 'system' }, language: { type: String, default: 'en' } }- เก็บข้อมูลแบบ nested (ซ้อนกัน)
- สามารถเข้าถึงได้ด้วย
user.preferences.theme
-
profile (โปรไฟล์)
profile: { bio: { type: String, default: '' }, avatar: { type: String, default: '' }, phone: { type: String, default: '' } }- เก็บข้อมูลโปรไฟล์ผู้ใช้
Timestamps
- timestamps: true
{ timestamps: true }- Mongoose จะเพิ่ม
createdAtและupdatedAtอัตโนมัติ createdAt: วันที่สร้างupdatedAt: วันที่แก้ไขล่าสุด
- Mongoose จะเพิ่ม
3. Note Model (โมเดลบันทึก)
ไฟล์: backend/src/models/Note.js
import mongoose from "mongoose";
const NoteSchema = new mongoose.Schema(
{
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
index: true,
required: true
},
title: { type: String, default: "" },
language: { type: String, default: "auto" }, // auto|th|en
audio: {
fileName: String,
url: String,
mimeType: String,
durationMs: Number
},
image: {
fileName: String,
url: String,
mimeType: String
},
transcript: { type: String, default: "" },
summary: {
lines: { type: [String], default: [] }, // 3–5 บรรทัด
bullets: { type: [String], default: [] }, // หัวข้อย่อย
keywords: { type: [String], default: [] } // keyword
},
quote: {
text: { type: String, default: "" },
style: { type: String, default: "inspire" }, // inspire|deep|sharp|funny
translation: { type: String, default: "" } // Thai translation
},
aiSource: { type: String, default: null },
sourceUrl: { type: String, default: "" }, // Link to source (if Google)
shareCount: { type: Number, default: 0 }, // Track social shares
viewCount: { type: Number, default: 0 }, // Track views/clicks
tags: { type: [String], default: [] }
},
{ timestamps: true }
);
export const Note = mongoose.model("Note", NoteSchema);
อธิบายฟีเจอร์พิเศษ:
1. Reference (ความสัมพันธ์)
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
index: true,
required: true
}
คำอธิบาย:
mongoose.Schema.Types.ObjectId: ชนิดข้อมูลเป็น ObjectId (ID ของ MongoDB)ref: "User": อ้างอิงไปยัง User Model- ใช้สำหรับ Populate (ดึงข้อมูล User มาด้วย)
ตัวอย่างการใช้งาน:
// ดึง Note พร้อมข้อมูล User
const note = await Note.findById(noteId).populate('userId');
console.log(note.userId.email); // เข้าถึง email ของ User ได้เลย
2. Array Types
summary: {
lines: { type: [String], default: [] },
bullets: { type: [String], default: [] },
keywords: { type: [String], default: [] }
}
คำอธิบาย:
[String]: Array ของ Stringdefault: []: ค่า default เป็น array ว่าง
ตัวอย่างข้อมูล:
{
"summary": {
"lines": ["บรรทัดที่ 1", "บรรทัดที่ 2"],
"bullets": ["หัวข้อ 1", "หัวข้อ 2"],
"keywords": ["คำสำคัญ1", "คำสำคัญ2"]
}
}
3. Number Type
shareCount: { type: Number, default: 0 },
viewCount: { type: Number, default: 0 }
คำอธิบาย:
- ใช้เก็บตัวเลข (Integer, Float)
- เหมาะสำหรับ counters, statistics
🔄 CRUD Operations (Create, Read, Update, Delete)
Create (สร้าง)
// สร้าง User ใหม่
const user = await User.create({
email: "test@example.com",
passwordHash: "hashed_password",
displayName: "Test User"
});
console.log(user._id); // MongoDB ObjectId
Read (อ่าน)
// หา User ทั้งหมด
const users = await User.find();
// หา User ตาม email
const user = await User.findOne({ email: "test@example.com" });
// หา User ตาม ID
const user = await User.findById(userId);
// หาด้วยเงื่อนไข
const admins = await User.find({ role: "admin" });
Update (แก้ไข)
// แก้ไขด้วย findByIdAndUpdate
const user = await User.findByIdAndUpdate(
userId,
{ displayName: "New Name" },
{ new: true } // return ข้อมูลใหม่
);
// แก้ไขหลายรายการ
await User.updateMany(
{ role: "user" },
{ $set: { "preferences.theme": "dark" } }
);
Delete (ลบ)
// ลบตาม ID
await User.findByIdAndDelete(userId);
// ลบตามเงื่อนไข
await User.deleteMany({ role: "user" });
🎓 Data Validation
Built-in Validators
const ProductSchema = new mongoose.Schema({
name: {
type: String,
required: [true, "Product name is required"],
minlength: [3, "Name must be at least 3 characters"],
maxlength: [100, "Name cannot exceed 100 characters"],
trim: true // ตัด whitespace
},
price: {
type: Number,
required: true,
min: [0, "Price cannot be negative"],
max: [1000000, "Price too high"]
},
email: {
type: String,
match: [/^\S+@\S+\.\S+$/, "Invalid email format"]
},
status: {
type: String,
enum: {
values: ["active", "inactive", "pending"],
message: "{VALUE} is not a valid status"
}
}
});
Custom Validators
const UserSchema = new mongoose.Schema({
age: {
type: Number,
validate: {
validator: function(v) {
return v >= 18 && v <= 100;
},
message: props => `${props.value} is not a valid age!`
}
}
});
🔗 Relationships (ความสัมพันธ์)
1. One-to-Many (1:N)
ตัวอย่าง: 1 User มีหลาย Notes
// User Model
const UserSchema = new mongoose.Schema({
email: String
});
// Note Model
const NoteSchema = new mongoose.Schema({
userId: { type: mongoose.Schema.Types.ObjectId, ref: "User" },
content: String
});
// ดึงข้อมูล
const note = await Note.findById(noteId).populate('userId');
console.log(note.userId.email);
2. Many-to-Many (N:M)
ตัวอย่าง: Students และ Courses
const StudentSchema = new mongoose.Schema({
name: String,
courses: [{ type: mongoose.Schema.Types.ObjectId, ref: "Course" }]
});
const CourseSchema = new mongoose.Schema({
title: String,
students: [{ type: mongoose.Schema.Types.ObjectId, ref: "Student" }]
});
🎯 Indexes (ดัชนี)
ทำไมต้องใช้ Index?
Index ช่วยให้ค้นหาข้อมูลเร็วขึ้น (เหมือนดัชนีในหนังสือ)
const UserSchema = new mongoose.Schema({
email: { type: String, index: true }, // Single field index
username: { type: String, unique: true }, // Unique index
});
// Compound Index (หลายฟิลด์)
UserSchema.index({ email: 1, role: 1 });
// Text Index (สำหรับ full-text search)
UserSchema.index({ bio: "text" });
เมื่อไหร่ควรใช้ Index:
- ✅ ฟิลด์ที่ใช้ค้นหาบ่อย (email, username)
- ✅ ฟิลด์ที่ใช้ sort
- ✅ Foreign keys (userId, productId)
ข้อควรระวัง:
- ⚠️ Index ทำให้ write operations ช้าลง
- ⚠️ ใช้ memory มากขึ้น
📝 แบบฝึกหัด
คำถามทบทวน
- MongoDB เก็บข้อมูลในรูปแบบอะไร?
- Mongoose Schema ใช้ทำอะไร?
required: trueและunique: trueต่างกันอย่างไร?timestamps: trueจะเพิ่มฟิลด์อะไรให้ Schema?- การ populate ใช้ทำอะไร?
โจทย์ปฏิบัติ
โจทย์ที่ 1: สร้าง Product Schema
// TODO: สร้าง Schema สำหรับ Product ที่มี:
// - name (String, required, min 3 chars)
// - price (Number, required, min 0)
// - category (String, enum: ["electronics", "clothing", "food"])
// - inStock (Boolean, default true)
// - tags (Array of Strings)
โจทย์ที่ 2: สร้าง Comment Schema ที่มี Relationship
// TODO: สร้าง Comment Schema ที่:
// - อ้างอิงไปยัง User (userId)
// - อ้างอิงไปยัง Note (noteId)
// - มี content (String, required)
// - มี timestamps
โจทย์ที่ 3: CRUD Operations
// TODO: เขียนโค้ดเพื่อ:
// 1. สร้าง User ใหม่
// 2. หา User ทั้งหมดที่เป็น admin
// 3. แก้ไข displayName ของ User
// 4. ลบ User ที่มี email ที่กำหนด
📌 สรุป
ในบทนี้เราได้เรียนรู้:
✅ MongoDB คือ NoSQL Database ที่เก็บข้อมูลแบบ Document
✅ Mongoose คือ ODM ที่ช่วยจัดการ MongoDB ใน Node.js
✅ Schema กำหนดโครงสร้างและ validation ของข้อมูล
✅ Model ใช้สำหรับ CRUD operations
✅ References ใช้สร้างความสัมพันธ์ระหว่าง Collections
✅ Indexes ช่วยให้ค้นหาข้อมูลเร็วขึ้น
ไฟล์ที่เกี่ยวข้อง
backend/src/config/db.js- Database connectionbackend/src/models/User.js- User modelbackend/src/models/Note.js- Note model
🔜 บทถัดไป
บทที่ 3: ระบบ Authentication (JWT & bcrypt)
เราจะเรียนรู้การสร้างระบบ Login/Register, Password Hashing, และ Token-based Authentication