Hướng dẫn mongodb id embedded document

1. Embedded Documents là gì?

Embedded documents (Tài liệu nhúng) là documents có lược đồ riêng và là 1 phần của documents khác. Hiểu đơn giản thì embedded documents là 1 field nằm trong 1 collection thay vì lưu dữ liệu kiểu References ta phải thiết kế 2 collection để thể hiện mối quan hệ One-to-Many.

Embedded documents có mọi đặc điểm như 1 model, ta có thể sử dụng validators, middleware,... để xử lý lỗi khi làm việc với kiểu dữ liệu này.

Trong Mongoose, Embedded documents được khai báo dưới dạng array trong collection chứa nó và như nói phía trên, nó sẽ có 1 lược đồ riêng nhưng nằm trong cùng 1 file với collection chính.

Ví dụ: Ta cần quản lý các Students và mỗi students sẽ có nhiều Projects khác nhau. Khi này ta sẽ có 1 file chứa lược đồ như sau:

var Projects = new Schema({
  title: String,
  unit: String
})


var Student = new Schema({
  name: String,
  age: Number,
  projects: [Projects]
})

mongoose.model('Student', Student);

Ta có thể thấy cặp key-value projects: [Projects] trong lược đồ Student, value là 1 array đặc biệt có các phương thức cụ thể để làm việc với Embedded Documents và array này là một thể hiện của DocumentArray. Ở các cặp key-value khác thì value là 1 kiểu dữ liệu được định nghĩa sẵn. Ta cần định nghĩa cho DocumentArray này bằng 1 lược đồ riêng.

Lưu ý: Lược đồ con luôn phải được xác định trước lược đồ chính.

2. So sánh Embedded Documents vs References

Trước khi so sánh giữa 2 kiểu mô hình dữ liệu thì ta xem qua với ví dụ phía trên khi sử dụng kiểu References thì xây dựng lược đồ như thế nào đã nhé.

Ta có 2 file: Project.js và Student.js

//Project.js
var Project = new Schema({
  title: String,
  unit: String,
  student: { type:Schema.ObjectId, ref: "Student" }
});

mongoose.model('Project', Project);
//Student.js
var Student = new Schema({
  name: String,
  age: Number
});

mongoose.model('Student', Student);

References lưu trữ các mối quan hệ giữa dữ liệu bằng cách link từ collection này sang collection khác (tạo tham chiếu đến thằng cha) thông qua ObjectIds. Ở đây, nếu ta có một bản dữ liệu trong từng Student thì ta lưu ObjectID của Student trên từng Project. Thuộc tính ref phải khớp chính xác với tên model trong định nghĩa model của chúng ta.

Cơ bản thì cách sử dụng 2 mô hình dữ liệu này trong Mongoose là như vậy. Mỗi kiểu có ưu, nhược điểm khác nhau:

Embedded DocumentsReferences
Ưu điểm - Truy vấn và cập nhật dữ liệu dễ dàng.
- Đạt hiệu suất cao trong việc đọc dữ liệu
- Có thể cung cấp linh hoạt hơn với truy vấn.
- Đạt hiệu suất cao trong việc ghi dữ liệu
Nhược điểm - Kích thước document lớn ảnh hưởng đến việc ghi dữ liệu vì mỗi document không thể vượt quá 16MB - Với các hệ thống có nhiều collections thì truy vấn sẽ khó khăn hơn, yêu cầu nhiều công việc hơn

Bài viết này nói về Embedded Document nên chúng ta sẽ tiếp tục với các thao tác cơ bản của mô hình dữ liệu này.

3. Thêm 1 Embedded Document vào 1 mảng

// retrieve my model
var Student = mongoose.model('Student');

// create a student
var student = new Student({ name: 'Cao Thanh Sang', age: 23 });

// create a project
student.projects.push({ title: 'Project 1', unit: 'NodeJs' });

student.save(function (err) {
    if (err) {
        console.log(err);
    } else {
        console.log('Success!');
    }
});

4. Xóa 1 Embedded Document

Student.findById(myId, function (err, student) {
    if (err) {
        console.log(err)
    } else {
        student.projects[0].remove();
        student.save(function (err) {
            // do something
        });
    };
});

5. Tìm Embedded Document theo id

DocumentArrays có method đặc biệt là id() để lọc các Embedded Document theo thuộc tính _id của chúng

student.projects.id(my_id).remove();
student.save(function (err) {
    // embedded project with id `my_id` removed!
});

Bài viết đến đây kết thúc rồi!!! Mình xin cảm ơn các bạn đã đọc bài. Nếu có sai sót hoặc cách diễn đạt của mình sai thì hãy comment phía dưới nhé.

Tài liệu tham khảo:

  1. https://mongoosejs.com/docs/2.7.x/docs/embedded-documents.html
  2. https://docs.mongodb.com/manual/core/data-modeling-introduction/

1. Embedded Documents là gì?

Embedded documents (Tài liệu nhúng) là documents có lược đồ riêng và là 1 phần của documents khác. Hiểu đơn giản thì embedded documents là 1 field nằm trong 1 collection thay vì lưu dữ liệu kiểu References ta phải thiết kế 2 collection để thể hiện mối quan hệ One-to-Many.

Nội dung chính

  • 1. Embedded Documents là gì?
  • 2. So sánh Embedded Documents vs References
  • 3. Thêm 1 Embedded Document vào 1 mảng
  • 4. Xóa 1 Embedded Document
  • 5. Tìm Embedded Document theo id

Embedded documents có mọi đặc điểm như 1 model, ta có thể sử dụng validators, middleware,... để xử lý lỗi khi làm việc với kiểu dữ liệu này.

Trong Mongoose, Embedded documents được khai báo dưới dạng array trong collection chứa nó và như nói phía trên, nó sẽ có 1 lược đồ riêng nhưng nằm trong cùng 1 file với collection chính.

Ví dụ: Ta cần quản lý các Students và mỗi students sẽ có nhiều Projects khác nhau. Khi này ta sẽ có 1 file chứa lược đồ như sau:

var Projects = new Schema({
  title: String,
  unit: String
})


var Student = new Schema({
  name: String,
  age: Number,
  projects: [Projects]
})

mongoose.model('Student', Student);

Ta có thể thấy cặp key-value projects: [Projects] trong lược đồ Student, value là 1 array đặc biệt có các phương thức cụ thể để làm việc với Embedded Documents và array này là một thể hiện của DocumentArray. Ở các cặp key-value khác thì value là 1 kiểu dữ liệu được định nghĩa sẵn. Ta cần định nghĩa cho DocumentArray này bằng 1 lược đồ riêng.

Lưu ý: Lược đồ con luôn phải được xác định trước lược đồ chính.

2. So sánh Embedded Documents vs References

Trước khi so sánh giữa 2 kiểu mô hình dữ liệu thì ta xem qua với ví dụ phía trên khi sử dụng kiểu References thì xây dựng lược đồ như thế nào đã nhé.

Ta có 2 file: Project.js và Student.js

//Project.js
var Project = new Schema({
  title: String,
  unit: String,
  student: { type:Schema.ObjectId, ref: "Student" }
});

mongoose.model('Project', Project);
//Student.js
var Student = new Schema({
  name: String,
  age: Number
});

mongoose.model('Student', Student);

References lưu trữ các mối quan hệ giữa dữ liệu bằng cách link từ collection này sang collection khác (tạo tham chiếu đến thằng cha) thông qua ObjectIds. Ở đây, nếu ta có một bản dữ liệu trong từng Student thì ta lưu ObjectID của Student trên từng Project. Thuộc tính ref phải khớp chính xác với tên model trong định nghĩa model của chúng ta.

Cơ bản thì cách sử dụng 2 mô hình dữ liệu này trong Mongoose là như vậy. Mỗi kiểu có ưu, nhược điểm khác nhau:

Embedded DocumentsReferences
Ưu điểm - Truy vấn và cập nhật dữ liệu dễ dàng.
- Đạt hiệu suất cao trong việc đọc dữ liệu
- Có thể cung cấp linh hoạt hơn với truy vấn.
- Đạt hiệu suất cao trong việc ghi dữ liệu
Nhược điểm - Kích thước document lớn ảnh hưởng đến việc ghi dữ liệu vì mỗi document không thể vượt quá 16MB - Với các hệ thống có nhiều collections thì truy vấn sẽ khó khăn hơn, yêu cầu nhiều công việc hơn

Bài viết này nói về Embedded Document nên chúng ta sẽ tiếp tục với các thao tác cơ bản của mô hình dữ liệu này.

3. Thêm 1 Embedded Document vào 1 mảng

// retrieve my model
var Student = mongoose.model('Student');

// create a student
var student = new Student({ name: 'Cao Thanh Sang', age: 23 });

// create a project
student.projects.push({ title: 'Project 1', unit: 'NodeJs' });

student.save(function (err) {
    if (err) {
        console.log(err);
    } else {
        console.log('Success!');
    }
});

4. Xóa 1 Embedded Document

Student.findById(myId, function (err, student) {
    if (err) {
        console.log(err)
    } else {
        student.projects[0].remove();
        student.save(function (err) {
            // do something
        });
    };
});

5. Tìm Embedded Document theo id

DocumentArrays có method đặc biệt là id() để lọc các Embedded Document theo thuộc tính _id của chúng

student.projects.id(my_id).remove();
student.save(function (err) {
    // embedded project with id `my_id` removed!
});

Bài viết đến đây kết thúc rồi!!! Mình xin cảm ơn các bạn đã đọc bài. Nếu có sai sót hoặc cách diễn đạt của mình sai thì hãy comment phía dưới nhé.

Tài liệu tham khảo:

  1. https://mongoosejs.com/docs/2.7.x/docs/embedded-documents.html
  2. https://docs.mongodb.com/manual/core/data-modeling-introduction/

Docs HomeMongoDB Manual

This page describes a data model that uses embedded documents to describe a one-to-many relationship between connected data. Embedding connected data in a single document can reduce the number of read operations required to obtain data. In general, you should structure your schema so your application receives all of its required information in a single read operation.

Consider the following example that maps patron and multiple address relationships. The example illustrates the advantage of embedding over referencing if you need to view many data entities in context of another. In this one-to-many relationship between patron and address data, the patron has multiple address entities.

In the normalized data model, the address documents contain a reference to the patron document.

// patron document
{
_id: "joe",
name: "Joe Bookreader"
}
// address documents
{
patron_id: "joe", // reference to patron document
street: "123 Fake Street",
city: "Faketon",
state: "MA",
zip: "12345"
}
{
patron_id: "joe",
street: "1 Some Other Street",
city: "Boston",
state: "MA",
zip: "12345"
}

If your application frequently retrieves the address data with the name information, then your application needs to issue multiple queries to resolve the references. A more optimal schema would be to embed the address data entities in the patron data, as in the following document:

{
"_id": "joe",
"name": "Joe Bookreader",
"addresses": [
{
"street": "123 Fake Street",
"city": "Faketon",
"state": "MA",
"zip": "12345"
},
{
"street": "1 Some Other Street",
"city": "Boston",
"state": "MA",
"zip": "12345"
}
]
}

With the embedded data model, your application can retrieve the complete patron information with one query.

A potential problem with the embedded document pattern is that it can lead to large documents, especially if the embedded field is unbounded. In this case, you can use the subset pattern to only access data which is required by the application, instead of the entire set of embedded data.

Consider an e-commerce site that has a list of reviews for a product:

{
"_id": 1,
"name": "Super Widget",
"description": "This is the most useful item in your toolbox.",
"price": { "value": NumberDecimal("119.99"), "currency": "USD" },
"reviews": [
{
"review_id": 786,
"review_author": "Kristina",
"review_text": "This is indeed an amazing widget.",
"published_date": ISODate("2019-02-18")
},
{
"review_id": 785,
"review_author": "Trina",
"review_text": "Nice product. Slow shipping.",
"published_date": ISODate("2019-02-17")
},
...
{
"review_id": 1,
"review_author": "Hans",
"review_text": "Meh, it's okay.",
"published_date": ISODate("2017-12-06")
}
]
}

The reviews are sorted in reverse chronological order. When a user visits a product page, the application loads the ten most recent reviews.

Instead of storing all of the reviews with the product, you can split the collection into two collections:

  • The product collection stores information on each product, including the product's ten most recent reviews:

    {
    "_id": 1,
    "name": "Super Widget",
    "description": "This is the most useful item in your toolbox.",
    "price": { "value": NumberDecimal("119.99"), "currency": "USD" },
    "reviews": [
    {
    "review_id": 786,
    "review_author": "Kristina",
    "review_text": "This is indeed an amazing widget.",
    "published_date": ISODate("2019-02-18")
    }
    ...
    {
    "review_id": 777,
    "review_author": "Pablo",
    "review_text": "Amazing!",
    "published_date": ISODate("2019-02-16")
    }
    ]
    }
  • The review collection stores all reviews. Each review contains a reference to the product for which it was written.

    {
    "review_id": 786,
    "product_id": 1,
    "review_author": "Kristina",
    "review_text": "This is indeed an amazing widget.",
    "published_date": ISODate("2019-02-18")
    }
    {
    "review_id": 785,
    "product_id": 1,
    "review_author": "Trina",
    "review_text": "Nice product. Slow shipping.",
    "published_date": ISODate("2019-02-17")
    }
    ...
    {
    "review_id": 1,
    "product_id": 1,
    "review_author": "Hans",
    "review_text": "Meh, it's okay.",
    "published_date": ISODate("2017-12-06")
    }

By storing the ten most recent reviews in the product collection, only the required subset of the overall data is returned in the call to the product collection. If a user wants to see additional reviews, the application makes a call to the review collection.

Tip

When considering where to split your data, the most frequently-accessed portion of the data should go in the collection that the application loads first. In this example, the schema is split at ten reviews because that is the number of reviews visible in the application by default.

Tip

See also:

Using smaller documents containing more frequently-accessed data reduces the overall size of the working set. These smaller documents result in improved read performance for the data that the application accesses most frequently.

However, the subset pattern results in data duplication. In the example, reviews are maintained in both the product collection and the reviews collection. Extra steps must be taken to ensure that the reviews are consistent between each collection. For example, when a customer edits their review, the application may need to make two write operations: one to update the product collection and one to update the reviews collection.

You must also implement logic in your application to ensure that the reviews in the product collection are always the ten most recent reviews for that product.

In addition to product reviews, the subset pattern can also be a good fit to store:

  • Comments on a blog post, when you only want to show the most recent or highest-rated comments by default.

  • Cast members in a movie, when you only want to show cast members with the largest roles by default.