NoSQL, MongoDB และ ODM

/ 7 min read

Share on social media

nosql-mongo-basic สามารถดู video ของหัวข้อนี้ก่อนได้ ดู video

NoSQL คืออะไร ?

มารู้จักกับ NoSQL ก่อน NoSQL ย่อมาจาก “Non SQL” (ซึ่งเอาจริงๆเหมือนจะเพี้ยนมาจาก Not only SQL) เกิดขึ้นในปี 1998 ที่มีความพยายามจะเอาตัวเองหลุดจาก standard SQL ในสมัยนั้น

NoSQL เป็น 1 ใน category ของ database management ที่จะไม่ใช้ภาษา SQL ในการ query และจัดการ data

NoSQL ถูก design สำหรับการจัดการ data ขนาดใหญ่แบบ “ไม่มี structure” จึงเหมาะกับ data ที่ต้องการความ flexible และ scalable จาก relational database ปกติ

  • flexible ในแง่ สามารถเก็บ data ในรูปแบบไหนก็ได้ภายใน 1 table (หรือ collection)
  • scalable ถูก design มาให้ทำ horizontal scale ได้ตั้งแต่ตอนคิด idea นี้แล้ว
  • performance NoSQL ส่วนใหญ่จะถูกเก็บในรูปแบบของ key-value ซึ่งมีความเร็วในการ lookup (ค้นหา) มากกว่า รวมถึง style data ที่ไม่ได้มีการ fix ไว้ สามารถประยุกต์ไปเก็บข้อมูลรูปแบบอื่นๆเพื่อ performance ที่ดีขึ้นได้

ซึ่ง NoSQL สามารถเก็บข้อมูลเป็น “อะไรก็ได้เลย” เช่น

  • Document-oriented databases:เก็บข้อมูลเป็น collection และ document รูปแบบ JSON (เช่น MongoDB ที่เรากำลังจะพูดถึง) หรือ XLM format

  • Key-value stores: เก็บคู่เป็น Key value คู่กันไว้ใน database เช่น Redis

  • Column-family stores: เก็บ data ในรูปแบบ columns แทนการเก็บเป็น rows. เช่น Apache Cassandra, HBase.

  • Graph databases: เก็บ data ในรูปแบบของ nodes และ edges (เส่นเชื่อมระหว่าง node) ของ graph. เช่น Neo4j, ArangoDB

ที่ตัดสินใจหยิบ MongoDB มาเป็นเพราะเป็น NoSQL ที่สำหรับผม ผมว่า “เข้าใจง่ายที่สุด” อีกหนึ่งตัว และเป็นตัวที่ค่อนข้างยอดฮิตมากสำหรับการยกตัวอย่าง NoSQL

MongoDB คืออะไร ?

เพื่อให้เกิดความเข้าใจสำหรับคนที่ยังไม่เคยเล่น NoSQL เราจะมาลองผ่าน MongoDB กัน

MongoDB คือ NoSQL database ที่ทำการเก็บ data ในรูปแบบของ JSON-like document. โดย data ใน MongoDB นั้นจะมีรูปแบบดังนี้

Database: A database in MongoDB is a container for collections.

  1. Document ใน MongoDB จะทำการจัดการ document และเก็บ data เอาไว้ในรูปแบบของ set ของ field - value ที่มีหน้าตาคล้าย JSON (1 document = 1 data)
  2. Collection คือกลุ่มของ documents (มองภาพง่ายๆ collection เหมือน table ใน SQL และ document เหมือน row ใน SQL) โดย collection นั้นจะไม่ทำการบังคับเอาไว้ว่าต้องมี schema แบบไหน = 1 collection สามารถมี document หน้าตา JSON ที่แตกต่างกันออกมาได้
  3. Database คือ database ใน mongoDB ที่ทำการเก็บ collection ทั้งหมดเอาไว้

Database
Collection 1
Collection 2
Document 1
Document 2
Document 3

โดยการจัดการ Data ใน MongoDB นั้นก็จะมีวิธีการจัดการอยุ่ 4 แบบใหญ่ๆคือ

  1. Insert = เพิ่ม data ใช้คำสั่งอย่าง .insert, .insertOne, .insertMany ได้
  2. Query = ดึง data ใช้คำสั่งอย่าง .find
  3. Update = แก้ไข data ใช้คำสั่งอย่าง .update, .updateOne, .updateMany
  4. Delete = ลบ data ใช้คำสั่งอย่าง .delete, .deleteOne, .deleteMany

มาเล่น MongoDB command กัน

run mongoDB ผ่าน docker-compose

version: "3"
services:
mongodb:
image: mongo
container_name: mongodb_container
environment:
MONGO_INITDB_ROOT_USERNAME: rootuser
MONGO_INITDB_ROOT_PASSWORD: rootpass
ports:
- "27017:27017"
volumes:
- mongodb_data_container:/data/db
volumes:
mongodb_data_container:

เมื่อ run ขึ้นมาได้ mongoDB จะออกมาที่ Port 27017 ให้ทำการ run command นี้เพื่อเข้าสู่ MongoDB

Terminal window
# เข้า container ของ mongo
docker exec -it mongo bash
# command เพื่อเข้าใจงาน mongo command ใน container mongo
mongosh -u rootuser -p

มาลองเล่น basic MongoDB commands ของ command ทั้ง 4 เคสกัน

1. Insert

  • ทำการ insert data 1 document เข้า collection users
Terminal window
db.users.insertOne({
username: "john_doe",
email: "john_doe@example.com",
age: 28
})

** ถ้าใช้ insert (หรือ insertMany) จะเท่ากับ insert หลายข้อมูล, ถ้าเป็น insertOne จะเป็นข้อมูลตัวเดียว

2. Query

  • ทำการ read data 1 document จาก collection users
Terminal window
db.users.find()

หากต้องการค้นหาข้อมูลจาก field ไหนใน JSON = ให้ใส่ field ที่ต้องการค้นหากับค่าที่ต้องการค้นหาไป เช่น เคสนี้หา field user_name ที่มี value คือ john_doe

Terminal window
db.users.find({ username: "john_doe" })

3. Update

  • ทำการ update data 1 document ใน collection users
  • โดยเราจะต้องทำการระบุ field ก่อนว่าเราจะ update ให้กับ document ไหนใน users (ผ่านท่าเดียวกันกับการค้นหาข้อมูลใน collection)

เช่น ต้องการ update field age ให้เป็น 30 กับ document ที่มี field username คือ john_doe จะได้ query หน้าตาประมาณนี้ออกมา

Terminal window
db.users.updateOne(
{ username: "john_doe" },
{ $set: { age: 30 } }
)

** ถ้าใช้ update (หรือ updateMany) จะเท่ากับ update หลายข้อมูล, ถ้าเป็น updateOne จะเป็นข้อมูลตัวเดียว

4. Delete

  • ทำการ delete data 1 document ใน collection users
  • โดยเราจะต้องทำการระบุ field ก่อนว่าเราจะ delete ให้กับ document ไหนใน users (ผ่านท่าเดียวกันกับการค้นหาข้อมูลใน collection)

เช่น เราจะทำการลบข้อมูลทั้งหมดที่มี age เท่ากับ 30

Terminal window
db.users.deleteMany({ age: 30 })

** ถ้าใช้ delete (หรือ deleteMany) จะเท่ากับ delete หลายข้อมูล, ถ้าเป็น deleteOne จะเป็นข้อมูลตัวเดียว

Relation และ aggregation

  • ใน mongoDB นั้นจะมี _id เป็น field unique key ที่เป็นเหมือน key ที่เป็นตัวแทนของแต่ละข้อมูล
  • เราสามารถนำ key นั้นไปทำ relation ระหว่าง database และใช้คำสั่ง aggregate ในการค้นหาแทนได้

เช่น เคสนี้เราจะหยิบ _id สัก 1 ตัวใน collection users นำไปสร้างใหม่ใน orders ใน field userId

Terminal window
db.orders.insertOne({
userId: ObjectId("5f50c31f1c372c7dc2061e11"),
product: "Laptop",
price: 1000,
publishDate: new Date("2023-10-25")
})

แล้วหลังจากนั้นใช้ aggregate ค้นหา order จาก userId ที่ได้ทำการ insert เข้าไปแทน

Terminal window
db.orders.aggregate([
{
$match: {
userId: ObjectId("5f50c31f1c372c7dc2061e11")
}
},
{
$lookup: {
from: "users",
localField: "userId",
foreignField: "_id",
as: "userDetails"
}
}
])

aggregation framework (ท่า aggregate) เป็น fetature ของ MongoDB ที่สามารถทำให้สร้างคำสั่งสำหรับการแปลง data และการ compute data แบบซับซ้อนใน database ได้

หลักการของ aggregation คือ มันจะทำการไป computation (query) ตรงๆใน database ก่อน แล้วนำ record ทั้งหมดที่ได้มารวมกัน + process ใหม่ (a pipeline of stages) และคืนกลับมาเป็น result ตามที่ query aggregate ระบุได้ (เทียบกับ SQL จะคล้ายๆกับ SQL GROUP BY clause)

Ref: https://www.mongodb.com/docs/manual/aggregation/

ต่อกับ Node ผ่าน Mongoose (ODM)

ทีนี้เพื่อให้จัดการง่ายผ่าน ภาษาหรือ Framework Backend ต่างๆ โลกเราได้พัฒนาสิ่งหนึ่งขึ้นมาคือ Object-Document Mapping (ODM)

ODM คืออะไร ?

ODM (Object-Document Mapping) คือ programming technique อย่างหนึ่งที่อำนวยความสะดวกในการจัดการระหว่างภาษาที่เป็น document-oriented databases (อย่าง MongoDB) และ object-oriented programming languages (OOP) โดยการปรับมุมมองของ collection, document ออกมาเป็น Object ออกมาแทน

ซึ่งปัจจุบันได้มี ODM libraries ที่สามารถสื่อสารกับ NoSQL ได้ผ่าน object ไว้หลากหลายตัว ซึ่งในแต่ละ database นั้นก็จะมีตัวที่เหมาะสมแตกต่างกันไป โดยในเคสของ MongoDB จะขอยกตัวอย่างกับ Mongoose ตัวที่ถือว่าเป็นอีกตัวที่ฮิตมากๆมาให้ทุกคนได้รู้จักกัน

Mongoose คืออะไร ?

Ref: https://mongoosejs.com/

Mongoose คือ popular ODM library ของฝั่ง MongoDB และ Node.js ซึ่งจะทำการเตรียมคำสั่งที่ใช้สำหรับจัดการกับ MongoDB ผ่านการจัดการด้วย schema-based เป็นหลัก (มอง collection เป็นเหมือน Schema object ที่ต้องทำการ defined เอาไว้ผ่าน code และทุกอย่างก็จัดการผ่าน Schema object แทน)

ซึ่ง Mongoose นั้นได้ทำการเตรียมไว้ทุกอย่างตั้งแต่การ casting (แปลงค่าข้อมูล), validation, query building, business logic hooks และของต่างๆอีกมากมาย

key feature ใหญ่ๆของ Mongoose

  1. Schemas Mongoose จะสามารถ define schema ของ document ได้ โดย schema คือการประกาศโครงสร้างที่จะเป็นการบอกว่า document ที่อยู่ใน collection นี้จะมีหน้าตาออกมาประมาณไหน โดยสามารถกำหนด type, default value, validators เอาไว้ได้เลย

  2. Models หลังจากที่ define schema เราจะสามารถ compile มาได้อยู่ในรูปแบบของ Model และ Model นั้นจะเป็นตัวแทนในการคุยกับ schema (ที่ไปจัดการกับ collection ใน MongoDB) ได้

  3. Validation Mongoose ได้เตรียม built-in validation เอาไว้ เพื่อใช้สำหรับการ validate ข้อมูลที่ถูกต้องก่อนเข้า schema ได้ เพื่อให้แน่ใจว่าเรามีการบันทึกข้อมูลที่ถูกต้องเข้าไป

  4. Queries Mongoose ได้เตรียมท่าสำหรับการ query เอาไว้พร้อมเรียบร้อยจากทั้ง 4 (+1) เคสที่เราเล่นผ่าน command มา (ซึ่งเดี๋ยวเราจะมาเล่นกันใน Session นี้)

  5. Population Mongoose ได้เตรียม feature กระจายข้อมูล (populate) เอาไว้ ในกรณีที่มีการ insert ข้อมูลต่าง collection เข้าไป

เช่น อย่าง code ด้านล่างนี้คือการกระจายข้อมูล author เข้า collection book โดยนำข้อมูลที่อยู่ใน author ทั้งหมดเข้าไปเก็บคู่กันไว้ใน collection book แทน

const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const authorSchema = new Schema({
name: String,
age: Number,
});
const bookSchema = new Schema({
title: String,
author: { type: Schema.Types.ObjectId, ref: "Author" },
});
const Author = mongoose.model("Author", authorSchema);
const Book = mongoose.model("Book", bookSchema);
mongoose.connect("mongodb://localhost/test", { useNewUrlParser: true, useUnifiedTopology: true });
const author = new Author({
name: "J.K. Rowling",
age: 55,
});
author.save((err) => {
if (err) throw err;
const book = new Book({
title: "Harry Potter and the Sorcerer's Stone",
author: author._id, // assign the _id from the author
});
book.save((err) => {
if (err) throw err;
// Now, let's retrieve the book with the full author document
Book.findOne({ title: "Harry Potter and the Sorcerer's Stone" })
.populate("author") // this will replace the author's id with the full author document
.exec((err, book) => {
if (err) throw err;
console.log("The author is %s", book.author.name);
// prints "The author is J.K. Rowling"
});
});
});

ข้อดีนี้ จะมีประโยชน์เอาไว้ใช้สำหรับเคสของการทำ stamp ข้อมูลอย่าง order ที่ต้องนำข้อมูลทั้งหมดมาประกอบกัน เพื่อเก็บไว้ใน document ตัวใหม่แทน

ลง Mongoose ใน project node

ทำการ init project node ขึ้นมา และสามารถลงผ่าน npm ได้เลย

Terminal window
ืnpm install mongoose

มาลอง Mongoose กัน

  • ทำการสร้าง schema มา 2 ตัวคือ users และ orders
const mongoose = require("mongoose");
const express = require("express");
const app = express();
app.use(express.json());
const mongoURI = "mongodb://rootuser:rootpass@localhost:27017/test?authSource=admin";
const userSchema = new mongoose.Schema({
username: String,
email: String,
age: Number,
orders: [{ type: mongoose.Schema.Types.ObjectId, ref: "Order" }],
});
const User = mongoose.model("User", userSchema);
const orderSchema = new mongoose.Schema({
product: String,
price: Number,
publishDate: { type: Date, default: Date.now },
user: { type: mongoose.Schema.Types.ObjectId, ref: "User" },
});
const Order = mongoose.model("Order", orderSchema);
/* ทำการใส่ code ตรงตำแหน่งนี้ */
const PORT = 8000;
app.listen(PORT, async () => {
try {
await mongoose.connect(mongoURI, { useNewUrlParser: true, useUnifiedTopology: true });
console.log("Successfully connected to MongoDB");
console.log(`Server is running on port ${PORT}`);
} catch (err) {
console.error("Error connecting to MongoDB:", err);
}
});

ทำการเพิ่ม code ให้แทรก order เข้าไปได้

app.post("/users", async (req, res) => {
try {
const { username, email, age, orders } = req.body;
const user = new User({ username, email, age });
await user.save();
if (orders && orders.length) {
const orderDocuments = orders.map((order) => {
return new Order({ ...order, user: user._id });
});
await Order.insertMany(orderDocuments);
user.orders = orderDocuments.map((order) => order._id);
await user.save();
}
res.status(201).json({ message: "User created", user });
} catch (err) {
res.status(500).json({ message: "Error creating user", err });
}
});

ลองยิงผ่าน postman ดูโดยใช้ object นี้

{
"username": "test",
"email": "test@mail.com",
"age": 24,
"orders": [
{
"title": "test",
"totalPrice": 200
}
]
}

ก็จะสามารถเก็บข้อมูลเข้าไปได้

  • เพิ่ม API สำหรับการ get data users และ order เข้าไป
app.get("/users", async (req, res) => {
try {
// หากต้องการข้อมูล order = ให้เพิ่ม populate เข้าไป
const users = await User.find().populate("orders");
// หากต้องการค้นหาก็ใส่ json ที่ต้องการค้นหาเข้าไปได้เลย
// const user = await User.findOne({ email: email }).populate('orders')
res.json(users);
} catch (err) {
res.status(500).send(err.message);
}
});

และนี่ก็คือตัวอย่างการใช้ Mongoose โดยประมาณ (ท่า update, delete จะคล้ายๆกันสามารถดูผ่าน document เพิ่มเติมได้)

เทียบกับ SQL เราควรพิจารณาใช้ NoSQL ยังไงดี

(กันสับสน เวลาเราบอกว่าใช้ SQL จะหมายถึงการใช้ Relational Database ด้วยนะ)

จากประสบการณ์ที่ใช้งานมาใน Database ทั้ง 2 แบบ ผมจะทำ checklist ให้ตามนี้

เราควรจะใช้ SQL เมื่อ

  1. Data Integrity คือสิ่งที่สำคัญมาก (พวกการทำ Transaction) = ใช้ SQL จะตอบโจทย์แน่นอน (NoSQL จะแล้วแต่ประเภท database)
  2. Structured Data data ต้องเป๊ะ = ใช้ SQL ก็จะมั่นใจแน่นอนกว่า
  3. Complex Queries = ใช้ SQL ต้องยอมรับ ถ้าอยากให้จบภายใน query เดียวได้ SQL ยังคงเก่งกว่าในหลายๆเรื่องมาก มันเลยยังคงสะดวกในเคสของการออก Report (พลัง join, union) ** แต่ performance นั่นอีกเรื่องนะ

เราควรจะใช้ NoSQL เมื่อ

  1. Scalability (รวมถึง data ใหญ่แน่ๆ) ถ้า database นี้ต้องเล่นกับ transaction หนักแน่ๆ = NoSQL จะ scale ง่ายกว่า
  2. Unstructured ถ้าข้อมูลมีความไม่แน่นอนในการเก็บข้อมูลสูง (เช่น user อาจจะมีหลากหลายประเภท จนข้อมูลเกิดความหลากหลาย) = NoSQL ตอบโจทย์กว่า
  3. Rapid Development เน้นขึ้นงานได้ไว = NoSQL จะตอบโจทย์กว่า

โจทย์ส่วนใหญ่ที่ผมมักจะเป็น

  • Database ที่เจอกับ user เยอะๆ = มักจะเป็น NoSQL
  • Database ที่ใช้งานในระบบหลังบ้าน, report หลังบ้าน = มักจะเป็น SQL

ข้อสังเกตอย่างหนึ่งคือ NoSQL เกิดขึ้นหลัง SQL หลายปี และเกิดมาเพื่อ “แก้ปัญหา” ที่ SQL มี ในเรื่องของ volume data และ ความหลากหลายของ data

แต่เอาเข้าจริงๆ Database ทั้ง 2 ประเภทสามารถใช้ทำงานได้ทั้งคู่ ตราบเท่าที่เราเข้าใจว่า เราจะจัดการกับมันยังไง ไม่ว่าจะเป็น Database ประเภทไหนก็ใช้งานได้ทั้งคู่เช่นเดิมนะครับ

Reference

https://www.helenjoscott.com/2022/01/29/mongod-mongo-mongosh-mongos-what-now/#:~:text=mongo%20and%20mongosh&text=When%20should%20you%20use%20mongo,newer%20MongoDB%20shell%20(%20mongosh%20).

Related Post

Share on social media