Mục đích của bài viết này nhằm chia sẽ với mọi người các kiến thức về MongoDB và Node.js. Bài viết dành cho những beginners backend đang có hứng thú với NoSQL. Hy vọng sau bài viết này mọi người sẽ hiểu chính xác MongoDB, Mongoose và cách nó hoạt động với Node.js. Cuối bài viết mình có chia sẻ một project nhỏ đã làm với MERN Stack (Các bài viết về react, node.js mình đã viết trước đó). Trong bài viết này ta sẽ trả lời các câu hỏi:
- Làm thế nào kết nối Node.js với cơ sở dữ liệu?
- MongoDB là gì? NoSQL là gì?
- Mongoose là gì?
- Cách cài đặt Mongoose trong Node.js.
- Định nghĩa model
- CRUD và các thao tác khác.
Kết nối với cơ sở dữ liệu
Không chỉ ở node.js mà với cả những ngôn ngữ backend như PHP, Java hay Python ta đều có hai cách để giao tiếp với phía cơ sở dữ liệu:
- Sử dụng ngôn ngữ truy vấn dạng thuần, vd như SQL.
- Sử dụng ODM/ORM, ODM (Object Data Model) hay ORM (Object Relational Model) là 1 kỹ thuật lập trình giúp ánh xạ các record dữ liệu trong hệ quản trị cơ sở dữ liệu sang dạng đối tượng đang định nghĩa trong ngôn ngữ lập trình.
Việc sử dụng ngôn ngữ truy vấn thuần giúp các thao tác nhanh hơn, nhưng bù lại sẽ khó khăn do câu lệnh phức tạp hơn. Trong khi đó dùng ODM/ORM viết code ít hơn, dễ hiểu hơn. Phù hợp các case CRUD (Create, Read, Update, Delete). Do đó ở bài viết này sẽ tập trung giao tiếp với cơ sở dữ liệu thông qua các ODM/ORM.
MongoDB là gì ?
MongoDB là một cơ sở dữ liệu thuộc chuẩn NoSQL, khác với những hệ RDBMS như MySQL, PostgreSQL hay Oracle (đọc thêm NoSQL vs SQL). Giống như những NoSQL khác, MongoDB sẽ không dùng bảng để lưu trữ dữ liệu, thay vào đó nó sẽ lưu dữ liệu bằng các document riêng biệt, dữ liệu được lưu dưới dạng JSON như sau:
Từ SQL sang MongoDB
Với những ai đã quen với tư duy của SQL họ sẽ gặp khó khăn khi chuyển qua các NoSQL. Để có thể đơn giản hoá các khái niệm ta có thể thực hiện so sánh các khái niệm trong Mongo với các SQL như sau:
- Database dễ thấy là ở cả hai chẳng có gì sai khác ở khái niệm. Trong MongoDB, database sẽ là nơi lưu trữ tất cả các collection để xử lý.
- Collection trong MongoDB tương tự như bảng trong SQL. Nó là nơi lưu trữ các document.
- Field tương tự với col ở SQL. Là các thuộc tính của dữ liệu.
- Document tương ứng với row của SQL. Nó sẽ là nơi nhận dữ liệu nhập vào.
Vì các document trong MongoDB là dạng JSON, nên nó có thể lưu trữ các đối tượng lồng nhau như sau:
Mongoose là gì ?
Mongoose là một thư viện mô hình hóa đối tượng (Object Data Model - ODM) cho MongoDB và Node.js. Nó quản lý mối quan hệ giữa dữ liệu, cung cấp sự xác nhận giản đồ và được sử dụng để dịch giữa các đối tượng trong mã và biểu diễn các đối tượng trong MongoDB. Ta có thể làm tất cả công việc mà MongoDB Driver làm được với Mongoose.
Cài đặt
Mongoose được hỗ trợ bởi NPM cho việc cài đặt package trong ứng dụng node.js. Bây giờ ta sẽ thử tạo ứng dụng kết nối với MongoDB thông qua Mongoose.
Hello World
Trước tiên, ta tạo ứng dụng Node.js với Express, một ứng dụng Hello World đơn giản, file app.js:
const express = require('express')
const app = express()
const port = process.env.PORT || 3000;
app.get('/', (req, res) => {
res.send('Hello world!');
})
app.listen(port, () => {
console.log(`listening on port ${port}!`);
})
Bây giờ ta sẽ tải mongoose:
yarn add mongoose
Kết nối
Cú pháp kết nối với MongoDB như sau:
await mongoose.connect('mongodb://localhost/my_database');
Tuy nhiên, để tiện cho việc gọi kết nối ta tạo một file db.js như sau:
const mongoose = require('mongoose');
const connectDB = async () => {
try {
const conn = await mongoose.connect('mongodb://localhost:27017/my_database', {
useUnifiedTopology: true,
useNewUrlParser: true,
useCreateIndex: true,
useFindAndModify: false,
});
console.log(`MongoDB connected: ${conn.connection.host}`);
} catch(error) {
console.error(`Error: ${error.message}`);
process.exit(1);
}
}
module.exports = connectDB;
Như vậy ta có thể gọi kết nối đến MongoDB trong app.js:
const connectDB = require('./config/db');
connectDB();
Khi thực hiện connect()
với MongoDB ở đoạn code trên, ta có thêm vào 4 thông số:
useUnifiedTopology
: Mặc định làfalse
. Đặt lại làtrue
để chọn công cụ quản lý kết nối với MongoDB Driver.useNewUrlParser
: MongoDB Driver hiện tại đã không còn hỗ trợ trình phân tích chuỗi kết nối cũ. Việc thêm cờuseNewUrlParser
cho phép người dùng quay trở lại trình phân tích cũ nếu trình phân tích mới gặp lỗi. Lưu ý trình phân tích chuỗi không hỗ trợ các kết nối không có cổng, nênmongodb://localhost/my_database
phải sửa lại thànhmongodb://localhost:27017/my_database
.useCreateIndex
: Mặc định làfalse
. Đặt lại làtrue
để Mongoose đánh index mặc định bằngcreateIndex()
thay vìensureIndex()
(MongoDB Driver đã không còn hỗ trợ).useFindAndModify
: Mặc định làtrue
. Đặt lại làfalse
để dùng đượcfindByOneAndUpdate()
vàfindOneAndRemove()
.
Định nghĩa model
Model là các đối tượng sẽ được Mongoose ánh xạ thành các collection trong MongoDB. Với model ta có thể định nghĩa cấu trúc document nhận vào trong MongoDB, thiết lập dữ liệu mặc định, các phương thức xác thực, … Mongoose cũng sẽ cung cấp cho model những interface cho tạo, truy vấn, cập nhật hay xoá dữ liệu. Để tạo Model trước tiên ta cần tạo schema, sau đó thông qua schema mà định nghĩa model.
Tạo Schema trong Mongoose:
const Schema = mongoose.Schema;
const ObjectId = Schema.ObjectId;
const BlogPost = new Schema({
author: ObjectId,
title: String,
body: String,
date: Date,
});
Trong Mongoose, các thuộc tính của Schema sẽ có những kiểu dữ liệu tiêu biểu sau:
- String : kiểu chuỗi, dành cho dữ liệu dạng văn bản, ký tự.
- Number : kiểu số, dành cho dữ liệu dạng số nguyên, số thực.
- Date : kiểu ngày giờ, dành cho dữ liệu thời gian.
- Buffer : kiểu bộ đệm, dù chủ yếu lưu trữ file.
- Boolean : kiểu nhị nguyên, chỉ có hai giá trị
true
vàfalse
. - Mixed : bất kỳ dạng dữ liệu nào.
- ObjectId : lưu trữ
_id
của model khác, dùng cho liên kết hai model với nhau. Tương tự FOREIGNKEY trong SQL. - Array: lưu trữ dữ liệu dạng mảng.
Với từng kiểu dữ liệu, ta có thể:
- Thiết lập trường thuộc tính là
required
(bắt buộc, tương tựallowNull=false
trong SQL) - Thiết lập giá trị mặc định
- Xác nhận dữ liệu nhập vào là hợp lệ hay không
Validator, Types & Defaults.
Mongoose hỗ trợ các phương thức xác nhận, để ràng buộc các thuộc tính trong Mongoose đáp ứng những yêu cầu dữ liệu bắt buộc. Một vài trong số chúng là:
1. required: khi sử dụng required
, giá trị của thuộc tính bắt buộc phải tồn tại.
requried: true
2. minlength: là một số, ràng buộc này sẽ kiểm tra giá trị độ dài dữ liệu nhập vào phải lớn hơn hoặc bằng minlength
.
minlength: 4
3. maxlength: ngược lại với minlength
, giá trị độ dài dữ liệu nhập vào phải nhỏ hơn hoặc bằng maxlength
.
maxlength: 4
4. unique: đảm bảo thông tin dữ liệu của thuộc tính không bị trùng.
unique: true
5. default: thiết lập giá trị mặc định cho thuộc tính
default: null
Ví dụ:
const mongoose = required('mongoose');
// Create Schema
const userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true,
minlength : 2,
maxlength: 20,
},
password: {
type: String,
required: true,
},
email: {
type: String,
required: true,
},
avatar: {
type: String,
default: `https://www.gravatar.com/avatar/6166e4g6e4ger6rg4e6g46er?d=wavatar`
},
isAdmin: {
type: Boolean,
required: true,
default: false,
},
created: {
type: Date,
default: Date.now
}
})
Notes: Trong trường hợp muốn tạo thời gian tự động, có thể sử dụng timestamp: true
, nó sẽ tự tạo hai phần createdAt và updatedAt.
Khi đã tạo schema hoàn tất, ta sẽ định nghĩa các model vào MongoDB như sau:
module.exports = mongoose.model("User", userSchema);
Load Model
Tại node.js, nếu ta muốn sử dụng lại model trên cho các thao tác tạo, xoá, sửa, truy vấn. Ta chỉ việc import nó như các module thông thường:
const User = require('./User');
CRUD
Create
Thông thường thì thao tác tạo dữ liệu sẽ được thực hiện khi nhận về một HTTP POST từ request. Điều này cũng đồng nghĩa luôn với việc dữ liệu mà ta sẽ cần để tạo model, nằm bên trong req.body
. Kể từ version Express 4.x, để có thể lấy dữ liệu từ HTTP POST, ta phài cài đặt package body-parser.
yarn add body-parser
Cách dùng body-parser trong express:
const express = require('express');
const app = express();
const bodyParser = require('body-parser');
// support parsing of application/json type post data
app.use(bodyParser.json());
//support parsing of application/x-www-form-urlencoded post data
app.use(bodyParser.urlencoded({ extended: true }));
Trong mongoose, có hai cách để tạo một Model từ dữ liệu nhập vào. Cách thứ nhất là sử dụng phương thức save()
.
const user = new User({
name: req.body.name,
email: req.body.email,
password: req.body.password,
});
await post.save();
Hoặc cách thứ hai là dùng create()
:
const { name, email, password } = req.body;
const user = await User.create({
name,
email,
password,
});
Sau khi tạo xong dữ liệu có trong MongoDB sẽ có dạng như sau:
Ở đây _id
sẽ được tạo mặc định, ứng với mỗi schema sẽ có một _id
được tạo mặc đinh. __v
ghi nhận số lần thay đổi cấu trúc của model.
Read
Thao tác đọc hấy lấy thông tin dữ liệu tương ứng với phương thức HTTP GET. Ở đây có hai dạng lấy thông tin là lấy nhiều đối tượng và lấy một đối tượng.
Lấy nhiều
Trong Mongoose, ta có các phương thức find()
hỗ trợ lấy nhiều đối tượng, cú pháp:
const users = await User.find()
res.json(users);
Ta lấy được nhiều đối tượng như sau:
Ta cũng có thể lọc dữ liệu với find()
, ví dụ như:
await User.find({ name: 'john', age: { $gte: 18 } })
Chỉ lấy User có tên john
và lớn hơn 18
tuổi.
Lấy một
Trong Mongoose có hai cách để lấy một đối tượng, thứ nhất là findOne()
Với findone()
ta sẽ tìm một đối tượng nào đấy ứng với thuộc tính yêu cầu.
const user = await User.findOne({
email: req.body.email
});
Cách thứ hai là findById()
, như đã nói MongoDB sẽ có _id
mặc định với từng trường dữ liệu nhập vào. Vì các _id
là duy nhất nên ta có thể tìm đối tượng với _id
của nó.
const user = await User.findById({
_id_: req.params.id
});
Updates
Để cập nhật dữ liệu trong Mongoose, ta có thể thực hiện bằng save()
, tương tự như lúc tạo:
const user = await User.findById(req.params.id);
if (user) {
user.name = req.body.name || user.name;
user.email = req.body.email || user.email;
await user.save();
}
Hoặc ta có thể thực hiện với findOneAndUpdate()
hoặc findByIdAndUpdate()
.
const user = await User.findByIdAndUpdate(req.params.id, {
name: req.body.name
}).exec();
const user = await User.findOneAndUpdate({ _id: req.params.id}, {
name: req.body.name
}).exec();
Delete
Cũng giống như Update, ta cũng có thể xoá dữ liệu thủ công bằng remove()
:
const user = await User.findById(req.params.id);
await user.remove();
Hoặc dùng các phương thức mà mongoose hỗ trợ:
await User.findByIdAndRemove(req.param.id)
// or
await User.findOneAndRemove({ _id: req.param.id })
Các thao tác khác
Trong Mongoose để tuỳ chỉnh dữ liệu ta có thể gộp các model với populate.
populate
Dùng để lấy dữ liệu từ các model có liên kết với nhau, ta có ví dụ sau:
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
username: String,
email: String
})
const postSchema = new mongoose.Schema({
title: String,
postedBy: {
type: mongoose.Schema.Types.ObjectId,
ref: "User"
}
})
const User = mongoose.model('User', userSchema);
const Post = mongoose.model('Post', postSchema);
Trong ví dụ trên Post liên kết với User thông qua postedBy
. Nếu như thông thường ta tìm kiếm Post bằng find()
như sau:
Post.find()
.then(p => console.log(p))
.catch(error => console.log(error));
Ta sẽ có kết quả:
Khi ta sử dụng populate()
, ta viết lại như sau:
Post.find()
.populate("postedBy")
.then(p=>console.log(p))
.catch(error=>console.log(error));
Lúc này dữ liệu ta nhận được sẽ có cả phần User liên kết qua postedBy
.
Ta cũng có thể tuỳ chỉnh thuộc tính ta nhận được từ liên kết, ví dụ như:
Post.find()
.populate("postedBy", "username")
.then(p=>console.log(p))
.catch(error=>console.log(error));
sort, limit & skip
Bên cạnh populate, khi thực hiện truy vấn dữ liệu ta cũng thể chọn lọc bằng các phương thức:
- sort : sắp xếp dữ liệu dựa trên thuộc tính.
- limit: giới hạn số document nhận về.
- skip: bỏ qua một số lượng document.
Ví dụ:
const posts = await Post.find({})
.sort({ createdAt: -1 })
.limit(5)
.skip(0)
Ở ví dụ trên ta sắp xếp dữ liệu nhận về theo thứ tự mới nhất (createdAt giảm dần), giới hạn 5 document và không bỏ qua document nào.
Tổng kết
Ở bài viết này mình giới thiệu cách kết nối và các thao tác cơ bản với Mongoose ở bài viết sau, ta sẽ tìm hiểu các chức năng nâng cao khác.