• Home
  • About
    • Ren photo

      Ren

      If I have seen further than others, it is by standing upon the shoulders of giants.

    • Learn More
    • Twitter
    • Facebook
    • LinkedIn
    • Instagram
    • Github
  • Posts
    • All Posts
    • All Tags
  • Projects

Kết Nối MongoDB Với Node.js Bằng Mongoose

11 Oct 2021

Reading time ~11 minutes

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:

document

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:

compare

  1. 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ý.
  2. Collection trong MongoDB tương tự như bảng trong SQL. Nó là nơi lưu trữ các document.
  3. Field tương tự với col ở SQL. Là các thuộc tính của dữ liệu.
  4. 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:

embed

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.

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ên mongodb://localhost/my_database phải sửa lại thành mongodb://localhost:27017/my_database.
  • useCreateIndex: Mặc định là false. Đặt lại là true để Mongoose đánh index mặc định bằng createIndex() thay vì ensureIndex() (MongoDB Driver đã không còn hỗ trợ).
  • useFindAndModify: Mặc định là true. Đặt lại là false để dùng được findByOneAndUpdate() 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:

  1. String : kiểu chuỗi, dành cho dữ liệu dạng văn bản, ký tự.
  2. Number : kiểu số, dành cho dữ liệu dạng số nguyên, số thực.
  3. Date : kiểu ngày giờ, dành cho dữ liệu thời gian.
  4. Buffer : kiểu bộ đệm, dù chủ yếu lưu trữ file.
  5. Boolean : kiểu nhị nguyên, chỉ có hai giá trị true và false.
  6. Mixed : bất kỳ dạng dữ liệu nào.
  7. 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.
  8. 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:

user

Ở đâ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:

json

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ả:

populate

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.

populate

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.

Tham khảo

afteracademy

arunrajeevan

geeksforgeeks



mongodbnode.jsmongoose Share Tweet +1