So sánh Mongoose vs MongoDB NodeJS Driver

So sánh Mongoose vs MongoDB NodeJS Driver

Trong bài viết này, chúng ta sẽ đi khám phá thư viện Mongoose của cơ sở dữ liệu MongoDB, liệu có đáng để sử dụng thay cho việc dùng MongoDB NodeJS Driver (tức là MongoDB thuần) không.

Chúng ta sẽ tìm hiểu và so sánh về các khía cạnh như:

  • Object Data Modeling trong MongoDB

  • Thêm validate Schema cho MongoDB

  • Populate và lookup

Oke, bắt đầu thôi nha.


🥇 Mongoose là gì?

Mongoose là một thư viện Object Data Modeling (ODM) - thư viện mô hình hóa dữ liệu đối tượng cho MongoDB và được xuất bản dưới dạng package npm.

Vấn đề mà Mongoose nhắm đến là giúp ae lập trình viên áp dụng được schema ở tầng ứng dụng (code backend nodejs) của chúng ta. Ngoài việc áp dụng schema, Mongoose cũng cung cấp một loạt các hook, validation model và các tính năng khác nhằm giúp làm cho việc làm việc với MongoDB dễ dàng hơn.


🥇 MongoDB Driver là gì?

MongoDB Driver là một thư viện được phát hành chính thức từ MongoDB giúp chúng ta làm việc với MongoDB. Thư viện này có sẵn trên hầu hết mọi ngôn ngữ lập trình, tất nhiên là bao gồm môi trường Node.js.

Dùng MongoDB Driver nghĩa là chúng ta dùng MongoDB thuần túy, không thông qua thư viện trung gian ODM như Mongoose.


🥇 Object Data Modeling trong MongoDB

Một lợi ích lớn của việc sử dụng cơ sở dữ liệu NoSQL như MongoDB là bạn không bị ràng buộc vào một mô hình dữ liệu cứng nhắc.

Bạn có thể thêm hoặc xóa các trường, lồng dữ liệu nhiều lớp miễn sao phù hợp với ứng dụng của bạn, ngoài ra nó cũng rất dễ dàng thay đổi trong tương lai.

Nhưng linh hoạt quá mức cũng có thể là một thách thức. Nếu không có sự đồng thuận về cấu trúc mô hình dữ liệu và mỗi document trong một collection chứa các trường khác nhau, chúng ta sẽ gặp khó khăn khi xử lý dữ liệu.

🥈 Mongoose Schema và Model

Trước khi bước vào so sánh thì chúng ta tìm hiểu sơ sơ về Schema và Model trong Mongoose đã nhé.

Khi dùng các ODM như Mongoose nó sẽ ép chúng ta vào một schema khá cứng nhắc. Với Mongoose bạn sẽ định nghĩa một object Schema trong code của mình, nó sẽ ánh xạ với document trong collection trong MongoDB. Sau đó bạn sẽ tạo một Model từ Schema trên - model này sẽ dùng để tương tác với collection.

Ví dụ chúng ta thiết kế bài viết cho một blog thì đầu tiên chúng ta sẽ xác định một schema và sau đó là tạo một model tương ứng

const blog = new Schema({
  title: String,
  slug: String,
  published: Boolean,
  content: String,
  tags: [String],
  comments: [
    {
      user: String,
      content: String,
      votes: Number
    }
  ]
})
 
const Blog = mongoose.model('Blog', blog)

Khi mà Mongoose model đã được định nghĩa, chúng ta có thể dễ dàng chạy các câu lệnh query để lấy, cập nhật và xóa dữ liệu trên một collection MongoDB.

Với đoạn code định nghĩa ở trên, chúng ta có thể làm

// Tạo mới một bài viết blog
const article = new Blog({
  title: 'Awesome Post!',
  slug: 'awesome-post',
  published: true,
  content: 'This is the best post ever',
  tags: ['featured', 'announcement']
})
 
// Insert bài viết vào MongoDB database
article.save()
 
// Tìm một bài viết
Blog.findOne({}, (err, post) => {
  console.log(post)
})

Bây giờ nếu chúng ta muốn lưu một data mới với kiểu dữ liệu như sau thì Mongoose sẽ báo lỗi, bởi vì nó không có thuộc tính author trong schema.

{
  title: 'Better Post!',
  slug: 'a-better-post',
  published: true,
  author: 'Ado Kukic',
  content: 'This is an even better post',
  tags: ['featured']
}

🥈 Custom model trong MongoDB Driver Nodejs

Nhưng nếu thao tác với MongoDB Driver Nodejs thì lại bình thường, không hề có lỗi lầm nào ở đây

db.collection('posts').insertOne({
  title: 'Better Post!',
  slug: 'a-better-post',
  published: true,
  author: 'Ado Kukic',
  content: 'This is an even better post',
  tags: ['featured']
})

Cơ bản là do MongoDB NodeJS Driver không có khái niệm gì về Model cả, không có nghĩa là chúng ta không thể tạo các model để đại diện cho dữ liệu MongoDB của chúng ta ở mức ứng dụng. Chúng ta có thể dễ dàng tạo một model chung bằng contructor function của javascript.

Chúng ta có thể tạo một model Blog như sau:

function Blog(post) {
   this.title = post.title;
   this.slug = post.slug;
   ...
}

Chúng ta có thể sử dụng model này kết hợp với MongoDB Node.js driver, cung cấp cho chúng ta sự linh hoạt khi sử dụng model nhưng không bị ràng buộc bởi nó.

db.collection('posts')
  .findOne({})
  .then((err, post) => {
    let article = new Blog(post)
  })

Trong ví dụ này, cơ sở dữ liệu MongoDB của chúng ta vẫn hoàn toàn không nhận thức được về Blog model ở tầng ứng dụng, nhưng ae dev có thể làm việc với nó, thêm các method và helper cho model và biết rằng model này chỉ được sử dụng trong phạm vi của ứng dụng Node.js của chúng ta.


🥇 Thêm Schema Validation

Xác thực Schema là điều cần thiết khi làm việc với database, để đảm bảo dữ liệu chèn vào database luôn thống nhất và chính xác.

Có 2 cách khác nhau để xác thực schema, tương đương 2 tầng

  1. Tầng ứng dụng

  2. Tầng database

Tầng database là quan trọng nhất, vì đó là chức năng sẵn có của database, anh em có đổi thư viện, đổi ngôn ngữ khác đi chăng nữa thì nó vẫn áp dụng được.

Còn tầng ứng dụng thì nó chỉ áp dụng được cho code hiện tại của anh em thôi, nếu đổi sang ngôn ngữ khác thì lại không có được. Đơn giản vì MongoDB không biết tầng này.

Mongoose áp dụng Schema Validation ở tầng ứng dụng, anh em có thể nhìn thử cái schema dưới đây, thuộc tính title có kiểu là String và bắt buộc truyền vào.

const blog = new Schema({
   title: {
       type: String,
       required: true,
   },
   slug: {
       type: String,
       required: true,
   },
   published: Boolean,
   content: {
       type: String,
       required: true,
       minlength: 250
   },
   ...
});
 
const Blog = mongoose.model('Blog', blog);

MongoDB Nodejs Driver không có cơ chế validation, vì vậy chúng ta phải định nghĩa schema validation cho MongoDB bằng cách sử dụng MongoDB Shell hoặc Compass

MongoDB Schema Validation là một tính năng có sẵn của cơ sỡ dữ liệu MongoDB, cho phép dễ dàng tạo schema cho MongoDB nhưng vẫn giữ được một mức độ linh hoạt cao, mang lại lợi ích tốt nhất giữa tầng ứng dụng - tầng cơ sở dữ liệu

Chúng ta có thể tạo Schema Validation khi tạo collection hoặc sau khi tạo collection đều được. Bây giờ mình sẽ tạo Schema Validation bằng cách dùng Compass kết hợp với MongoDB Atlas.

Để tìm hiểu về Schema Validation, các bạn có thể đọc series này

Tạo một collection posts và chèn 2 document này vào

[
  {
    "title": "Better Post!",
    "slug": "a-better-post",
    "published": true,
    "author": "Ado Kukic",
    "content": "This is an even better post",
    "tags": ["featured"]
  },
  {
    "_id": { "$oid": "5e714da7f3a665d9804e6506" },
    "title": "Awesome Post",
    "slug": "awesome-post",
    "published": true,
    "content": "This is an awesome post",
    "tags": ["featured", "announcement"]
  }
]

Trong UI phần mềm Compass, mình sẽ di chuyển đến tab Validation. Hiển nhiên là không có bất kỳ một validate rule nào ở đây cả, điều này nghĩa là dữ liệu nào chèn vào nó cũng cho.

Valid Document Schema

Valid Document Schema

Nhấn vào button Add a Rule để thêm rule cho nó. Cùng thêm require cho thuộc tính author nhé. Nó sẽ trông như thế này

{
  $jsonSchema: {
    bsonType: "object",
    required: [ "author" ]
  }
}

Bây giờ chúng ta sẽ thấy bài post khởi tạo của chúng ta, cái nào không có trường author sẽ fail validation, cái nào có author thì sẽ qua được.

Invalid Document Schema

Invalid Document Schema

Chúng ta có thể thêm validation cho các trường khác bằng cách cập nhật lại schema validation như thế này

{
  $jsonSchema: {
    bsonType: "object",
    required: [ "tags" ],
    properties: {
      title: {
        bsonType: "string",
        minLength: 20,
        maxLength: 80
      }
    }
  }
}

title phải có độ dài từ 20 - 80 ký tự.

Có rất là nhiều rule cho chúng ta thêm vào. Để xem chi tiết, các bạn có thể click vào đây. Nâng cao hơn thì nên đọc những bài này để biết thêm về schema validation với array và depedencies.

Thêm nữa về Schema Validation trong MongoDB rất là linh động. Khi chúng ta thêm một schema, validation trên những document tồn tại trước đó sẽ không tự động thực hiện. Validation chỉ thực hiện ở những lệnh update hay insert. Nếu chúng ta muốn để nguyên các document hiện có, chúng ta có thể thay đổi validationLevel để chỉ có tác dụng với những document mới được thêm vào database.

Khi fail validation, nó sẽ cho ra một error, nếu chỉ muốn warn thôi thì đơn giản chỉ cần thay đổi validationAction là được.

Và cuối cùng nếu cần, chúng ta có thể bypass document validation bằng cách thêm option bypassDocumentValidation

db.collection('posts').insertOne({ title: 'Awesome' }, { bypassDocumentValidation: true })

🥇 Populate và Lookup

Cái cuối cùng mà mình muốn so sánh giữa Mongoose và MongoDB NodeJS Driver là hỗ trợ pseudo-joins. Cả 2 thằng đều hỗ trợ điều này, chúng ta có thể kết hợp nhiều document từ nhiều collection trong một database lại với nhau, điều này tương tự như cách chúng ta join trong cơ sở dữ liệu quan hệ.

Cách tiếp cận của Mongoose gọi là Populate. Nó cho phép chúng ta tạo các model mà tham chiếu lẫn nhau và từ đó có thể kết hợp để cho ra kết quả tương ứng.

Ví dụ dưới đây, user trong comment của blog là một _id, nó sẽ tham chiếu đến collection là User.

const user = new Schema({
   name: String,
   email: String
});
 
const blog = new Schema({
   title: String,
   slug: String,
   published: Boolean,
   content: String,
   tags: [String],
   comments: [{
       user: { Schema.Types.ObjectId, ref: 'User' },
       content: String,
       votes: Number
   }]
});
 
const User = mongoose.model('User', user);
const Blog = mongoose.model('Blog', blog);

comments nó sẽ có dạng như dưới đây

comments: [{ user: '12345', content: 'Great Post!!!' }]

12345_id của một document User nào đó. Khi chúng ta đọc một document trong collection Blog thì chúng ta sẽ không thấy được thông tin như user.name hay user.email trong comments. Để thực hiện được điều này thì chúng ta cần phải Populate.

Blog.findOne({})
  .populate('comments.user')
  .exec(function (err, post) {
    console.log(post.comments[0].user.name) // Tên của user tại comment thứ nhất
  })

Populate là một tính năng rất hay của Mongoose, nhưng bạn sẽ chẳng biết nó thực hiện điều gì ở bên dưới. Nếu bạn cần những câu lệnh query phức tạp thì có thể nó sẽ không tối ưu bằng việc bạn dùng MongoDB Driver.

Một vấn đề khác của việc này là nó chỉ hoạt động ở tầng ứng dụng, nếu dựa dẫm vào nó, bạn sẽ không biết tự viết các câu lệnh nâng cao, và tương lai sẽ cắn bạn một cái thật đau.

Trong khí đó MongoDB từ version 3.2 trở lên hỗ trợ $lookup cho phép ae dev join các collection trong một database dễ dàng. Với ví dụ trên chúng ta có thể làm như sau

db.collection('posts').aggregate(
  [
    {
      $lookup: {
        from: 'users',
        localField: 'comments.user',
        foreignField: '_id',
        as: 'users'
      }
    },
    {}
  ],
  (err, post) => {
    console.log(post.users) // mảng users
  }
)

Với cách viết thuần túy như thế này, chúng ta có thể tùy biến rất sâu, tối ưu được performance và phát huy được hết sức mạnh của MongoDB.

Các bạn có thể tìm hiểu thêm về aggregation tại đây


🥇 Cuối cùng thì chúng ta có thực sự cần Mongoose?

Mình sẽ tóm tắt lại bài viết này như sau

Ưu điểm và nhược điểm khi dùng Mongoose

✅ Ưu điểm:

  • Có schema và model tường minh

  • Có sẵn schema validation ở tầng ứng dụng giúp tăng cường thêm tính nhất quán cho database

  • Cung cấp các hàm API để thao tác với MongoDB một cách dễ dàng

❌ Nhược điểm:

  • Tốn thời gian học thêm thư viện

  • Bị giới hạn bởi thư viện, tính tùy biến không cao bằng việc sử dụng MongoDB Driver dẫn đến hiệu suất sẽ chậm hơn khi thực hiện các câu lệnh phức tạp

  • Việc tương tác với MongoDB qua Mongoose có thể tạo ra các lỗi hoặc xung đột bởi vì các hàm API được định nghĩa bởi Mongoose, vì nó không được phát hành chính thức bởi MongoDB.

Ưu điểm và nhược điểm khi dùng MongoDB NodeJS Driver

✅ Ưu điểm:

  • Hàng chính chủ của cơ sở dữ liệu MongoDB, document đầy đủ và chi tiết, nếu gặp lỗi thì có thể dễ dàng tìm hướng giải quyết hơn là khi dùng Mongoose

  • Không cần học thêm thư viện ODM ngoài

  • Viết câu lệnh có thể dùng được ở nhiều môi trường khác nhau như MongoDB Shell (thực ra thì có một số câu lệnh hơi khác 1 tí nhưng đa số là giống)

  • Tính tùy biến cao

  • Sử dụng được mọi chức năng sẵn có của cơ sở dữ liệu MongoDB mà không bị giới hạn nào

❌ Nhược điểm:

  • Không có Schema Validation ở tầng ứng dụng, cá nhân mình thì không cần cái này lắm, vì mình có validate đầu vào bằng express-validator rồi. Cái quan trọng là Schema Validation ở tầng database thôi.

  • Câu lệnh thao tác với database sẽ dài hơn 1 tý so với Mongoose

  • Bạn phải tự setup nhiều thứ nếu muốn nó chặt chẽ và có quy cũ (cái này thì không thành vấn đề đâu, dễ ồm à)

Cá nhân mình thì không cần một ODM như Mongoose, mình thích dùng MongoDB Driver hơn. Điều này cho phép mình tận dụng được hết sức mạnh của database.

Và mình nghĩ các bạn mới học MongoDB thì nên dùng MongoDB Driver để hiểu rõ hơn về cơ sở dữ liệu này, thay vì dùng Mongoose để tạo ra các model, schema, ... mà không biết nó thực hiện điều gì ở bên dưới.

Các bạn có thể đọc thêm suy nghĩ của các lập trình viên về Mongoose hay MongoDB Driver tại đây: https://stackoverflow.com/questions/18531696/why-should-we-use-mongoose-odm-instead-of-using-directly-mongodb-with-mongodb-dr

Còn các bạn thì sao? Các bạn có thích dùng ODM như Mongoose không? Hãy comment dưới bài viết để cho mình biết nhé!

Tham khảo:

Bài viết có tham khảo từ MongoDB & Mongoose: Compatibility and Comparison

Nguồn bài viết: https://duthanhduoc.com

Phạm Hồng Đức
Phạm Hồng Đức
Một developer thích nghiên cứu và chia sẻ kiến thức lập trình, phần mềm mã nguồn mở (open-source), và cuộc sống.