บทที่ 2backend|18 KB

บทที่ 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");
}

อธิบายทีละบรรทัด:

  1. import mongoose from "mongoose"

    • นำเข้า Mongoose library
  2. export async function connectDB()

    • สร้างฟังก์ชัน async เพื่อเชื่อมต่อ Database
    • async หมายความว่าฟังก์ชันนี้ทำงานแบบ asynchronous
  3. const uri = process.env.MONGO_URI

    • อ่าน MongoDB connection string จาก Environment Variable
    • ตัวอย่าง URI:
      mongodb://localhost:27017/quom
      mongodb+srv://user:pass@cluster.mongodb.net/quom
      
  4. if (!uri) throw new Error("Missing MONGO_URI")

    • ตรวจสอบว่ามี URI หรือไม่
    • ถ้าไม่มี ให้ throw Error เพื่อหยุดการทำงาน
  5. await mongoose.connect(uri)

    • เชื่อมต่อกับ MongoDB
    • await หมายความว่ารอให้เชื่อมต่อเสร็จก่อนทำงานต่อ
  6. 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

  1. email

    email: { type: String, unique: true, required: true, index: true }
    
    • type: String: ชนิดข้อมูลเป็น String
    • unique: true: ห้ามซ้ำ (แต่ละ email ต้องไม่ซ้ำกัน)
    • required: true: จำเป็นต้องมี (ห้ามเป็น null)
    • index: true: สร้าง index เพื่อค้นหาเร็วขึ้น
  2. passwordHash

    passwordHash: { type: String, required: true }
    
    • เก็บรหัสผ่านที่ hash แล้ว (ไม่เก็บ plain text)
    • จำเป็นต้องมี
  3. displayName

    displayName: { type: String, default: "" }
    
    • default: "": ถ้าไม่ระบุ จะใช้ค่า default เป็น empty string
  4. role (Enum)

    role: { type: String, enum: ["user", "admin"], default: "user" }
    
    • enum: ["user", "admin"]: จำกัดค่าที่เป็นไปได้เฉพาะ "user" หรือ "admin"
    • ถ้าใส่ค่าอื่นจะ error

Nested Objects (Embedded Documents)

  1. 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
  2. profile (โปรไฟล์)

    profile: {
      bio: { type: String, default: '' },
      avatar: { type: String, default: '' },
      phone: { type: String, default: '' }
    }
    
    • เก็บข้อมูลโปรไฟล์ผู้ใช้

Timestamps

  1. timestamps: true
    { timestamps: true }
    
    • Mongoose จะเพิ่ม createdAt และ updatedAt อัตโนมัติ
    • createdAt: วันที่สร้าง
    • updatedAt: วันที่แก้ไขล่าสุด

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 ของ String
  • default: []: ค่า 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 มากขึ้น

📝 แบบฝึกหัด

คำถามทบทวน

  1. MongoDB เก็บข้อมูลในรูปแบบอะไร?
  2. Mongoose Schema ใช้ทำอะไร?
  3. required: true และ unique: true ต่างกันอย่างไร?
  4. timestamps: true จะเพิ่มฟิลด์อะไรให้ Schema?
  5. การ 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 ช่วยให้ค้นหาข้อมูลเร็วขึ้น

ไฟล์ที่เกี่ยวข้อง


🔜 บทถัดไป

บทที่ 3: ระบบ Authentication (JWT & bcrypt)
เราจะเรียนรู้การสร้างระบบ Login/Register, Password Hashing, และ Token-based Authentication


📚 แหล่งเรียนรู้เพิ่มเติม