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
, String
và Buffer
đề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 blogPost
và product
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