+26

Tìm hiểu về Populate trong Mongoogse

Trong MongoDB sử dụng $lookup aggregation operator để join-like các document với nhau. Và Mongoose - elegant mongodb object modeling for node.js có thể gọi là một orm của nodejs và mongodb, nó hỗ trợ một phương thức rất tuyệt vời để join-like là populate().
Population có thể tự động tạo đường dẫn với các document với nhau. Nó có liên kết 1 document, nhiều document, các đối tượng (object) đơn giản, các đối tượng lồng nhau hoặc là trả tất cả các đối tượng trong 1 lần truy vấn. Chúng ta có thể xem một ví dụ dưới đây.

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const personSchema = Schema({
  _id: Schema.Types.ObjectId,
  name: String,
  age: Number,
  stories: [{ type: Schema.Types.ObjectId, ref: 'Story' }]
});

const storySchema = Schema({
  author: { type: Schema.Types.ObjectId, ref: 'Person' },
  title: String,
  fans: [{ type: Schema.Types.ObjectId, ref: 'Person' }]
});

const Story = mongoose.model('Story', storySchema);
const Person = mongoose.model('Person', personSchema);

Chúng ta tạo 2 Model. Model Person của chúng ta có trường stories là một mảng ObjectId, ref là một option để cho mongoose có thể hiểu là nó liên kết với model nào và ở đây là Story. All _id của mà chúng ta lưu ở trường stories phải nằm trong _id của Story model. ObjectId, Number, StringBuffer đều có thể là ref, nhưng để tối ưu tốc độ truy vấn thì chúng ta lên dùng ObjectId.

1. Saving refs

Để lưu refs, cũng giống như là lưu một document thông thường, chúng ta chỉ cần gián giá trị _id:

const author = new Person({
  _id: new mongoose.Types.ObjectId(),
  name: 'Ian Fleming',
  age: 50
});

author.save(function (err) {
  if (err) return handleError(err);

  const story1 = new Story({
    title: 'Casino Royale',
    author: author._id    // gián giá trị _id cho person
  });

  story1.save(function (err) {
    if (err) return handleError(err);
  });
});

2. Population

Không có gì đặc biệt cả, chúng ta chỉ tạo một Person và một Story. Và tìm tác giả của truyện có tựa là Casino Royale chúng ta có thể truy vấn như sau:

Story.
  findOne({ title: 'Casino Royale' }).
  populate('author').
  exec(function (err, story) {
    if (err) return handleError(err);
    console.log(story);
  });

kết quả trả về có dạng như sau:

{
    _id: ..,
    title: 'Casino Royale',
    author: {
        _id: ..,
        name: 'Ian Fleming',
        age: 50
    }
}

3. Nếu không có trường trung gian

Mongoose populate không hoạt động giống như join giống như SQL join. Khi mà không có document, story.author sẽ trả về null. Nó hoạt động giống như left join trong sql.

await Person.deleteMany({ name: 'Ian Fleming' });

const story = await Story.findOne({ title: 'Casino Royale' }).populate('author');
story.author;

Nêu chúng ta có một mảng trong authors trong storySchema, populate() sẽ trả về một mảng rỗng

const storySchema = Schema({
  authors: [{ type: Schema.Types.ObjectId, ref: 'Person' }],
  title: String
});

const story = await Story.findOne({ title: 'Casino Royale' }).populate('authors');
story.authors; // `[]`

4. Select trường và Populating Multiple Paths

Population cho phép chúng ta chọn các trường trong model tương ứng.

Story.
  findOne({ title: /casino royale/i }).
  populate('author', 'name'). // chỉ trả về name, và giá trị _id
  exec(function (err, story) {
    if (err) return handleError(err);

    console.log('The author is %s', story.author.name);
    // prints "The author is Ian Fleming"

    console.log('The authors age is %s', story.author.age);
    // prints "The authors age is null'
  });

chúng ta có thể populate nhiều path trong cùng một lần truy vấn

Story.
  find(...).
  populate('fans').
  populate('author').
  exec();

hoặc chúng ta có thể viết là

Story.
  find(...).
  populate({path: 'fans'}).
  populate({path: 'author'}).
  exec();

hay gọn hơn nữa là:

Story.
  find(...).
  populate([{path: 'fans'}, {path: 'author'}]).
  exec();

Chúng ta cũng có thể thêm các otions và query ví dụ ta tìm kiếm story có tác giả tuổi lớn hơn hoặc bằng 21, lấy tối đa 5 người, và chỉ lấy mỗi tên

Story.
  find(...).
  populate({
    path: 'fans',
    match: { age: { $gte: 21 }},
    select: 'name -_id',
    options: { limit: 5 }
  }).
  exec();

5. Populating nhiều cấp

Ví dụ tao có một schema User theo dõi một user khác.

var userSchema = new Schema({
  name: String,
  friends: [{ type: ObjectId, ref: 'User' }]
});

chúng ta có thể truy vấn bạn của tất cả bạn của chúng ta như sau.

User.
  findOne({ name: 'Val' }).
  populate({
    path: 'friends',
    populate: { path: 'friends' }
  });

6. Populating thông qua database khác nhau

Chúng ta có 2 model, là event và conversation, cứ mỗi conversation ứng với một event tương ứng

var eventSchema = new Schema({
  name: String,
  conversation: ObjectId
});
var conversationSchema = new Schema({
  numMessages: Number
});

Hai model này nằm ở hai database khác nhau

var db1 = mongoose.createConnection('localhost:27000/db1');
var db2 = mongoose.createConnection('localhost:27001/db2');

var Event = db1.model('Event', eventSchema);
var Conversation = db2.model('Conversation', conversationSchema);

theo cách thông thường chúng ta không thể populate hai model vơi nhau được, trừ khi bạn chỉ rõ model

Event.
  find().
  populate({ path: 'conversation', model: Conversation }).
  exec(function(error, docs) { });

7. Liên kết động với refPath

Mongoose cho phép chúng ta liên kết với nhiều collections thông qua trường giá trong model.

const commentSchema = new Schema({
  body: { type: String, required: true },
  on: {
    type: Schema.Types.ObjectId,
    required: true,
    refPath: 'onModel'
  },
  onModel: {
    type: String,
    required: true,
    enum: ['BlogPost', 'Product']
  }
});

const Product = mongoose.model('Product', new Schema({ name: String }));
const BlogPost = mongoose.model('BlogPost', new Schema({ title: String }));
const Comment = mongoose.model('Comment', commentSchema);

refPath là một thay thế cho ref, nhưng ref đơn thuần chỉ là một string cố định còn refPath thì nó có thể cấu hình thông qua model Mongoose

const book = await Product.create({ name: 'The Count of Monte Cristo' });
const post = await BlogPost.create({ title: 'Top 10 French Novels' });

const commentOnBook = await Comment.create({
  body: 'Great read',
  on: book._id,
  onModel: 'Product'
});

const commentOnPost = await Comment.create({
  body: 'Very informative',
  on: post._id,
  onModel: 'BlogPost'
});

// The below `populate()` works even though one comment references the
// 'Product' collection and the other references the 'BlogPost' collection.
const comments = await Comment.find().populate('on').sort({ body: 1 });
comments[0].on.name; // "The Count of Monte Cristo"
comments[1].on.title; // "Top 10 French Novels"

Chúng ta có cách làm khác là phân chia thành 2 trường blogPostproduct trong commentSchema và populate() cả hai

const commentSchema = new Schema({
  body: { type: String, required: true },
  product: {
    type: Schema.Types.ObjectId,
    required: true,
    ref: 'Product'
  },
  blogPost: {
    type: Schema.Types.ObjectId,
    required: true,
    ref: 'BlogPost'
  }
});

// ...

// The below `populate()` is equivalent to the `refPath` approach, you
// just need to make sure you `populate()` both `product` and `blogPost`.
const comments = await Comment.find().
  populate('product').
  populate('blogPost').
  sort({ body: 1 });
comments[0].product.name; // "The Count of Monte Cristo"
comments[1].blogPost.title; // "Top 10 French Novels"

Nhưng cách làm này rất hạn chế, nếu chúng ta muốn populate() với nhiều trường khác nhau thì chúng cần hiều field để lưu id hơn.

Tài liệu tham khảo link


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí