Trong những phần trước chúng ta đã làm việc với những kiểu dữ liệu array cũng như objectID để khai báo những mối quan hệ của model đó. Để và truy vấn đến được các mối quan hệ này mà không cần phải sử dụng đến model của đối tượng đó thì thằng mongoose đã hỗ trợ cho chúng ta Populate. Nó giúp chúng ta nhanh chóng truy vấn đến những dữ liệu này.

Những bài trước chúng ta cũng đã sử dụng ở mức căn bản để lấy user, người đã viết bài hay bài viết của 1 user.

Trong bài hôm nay chúng ta sẽ đi sâu vào và tìm hiểu những tính năng mà nó hỗ trợ cho chúng ta. Sau này mình sẽ hướng dẫn các bạn nâng cao hơn để hỗ trợ nhiều nhu cầu trong thực tế. Các bạn có thể tự tìm hiểu về “aggregate” trước để hiểu thêm về nó.

Theo dõi source code tại đâybranch dev nha

Link postman để test cho dễ.

Khai báo

Chúng ta sử dụng ref trong model để cho mongoose hiểu là nó liên kết đến model nào. Và ở đây chúng ta đang tạo liên kết giữa Post và Category mình đã khai báo trong phần trước. Trong 1 model có thể khai báo nhiều ref, không vấn đề gì, khai báo ref cho field bình thường hoặc array, hoặc object bên trong array đều được nha. Mình đã thử và đã thành công, có điều handle hơi cồng kềnh, array là đủ rồi.

Và giá trị của Ref ta đã map nó khi khai báo bên trong Module á nha. Đừng khai báo trùng tên, mình cũng không biết nó sẽ xảy ra vấn đề gì đâu. Bạn thử xem sao!

@Module({
  imports: [
    MongooseModule.forFeature([
      {
        name: 'Post',
        schema: PostSchema,
      },
      {
        name: 'Category',
        schema: CategorySchema,
      },
    ]),
...
const PostSchema = new Schema(
  {
    title: String,
    description: String,
    content: String,
    user: {
      type: Schema.Types.ObjectId,
      ref: 'User',
    },
    tags: [String],
    numbers: [Number],
    categories: [{ type: Schema.Types.ObjectId, ref: 'Category' }],
  },
  {
    timestamps: true,
    collection: 'posts',
  },
);

export { PostSchema };

export interface Post extends Document {
  title: string;
  description: string;
  content: string;
  user: User;
  tags: [string];
  numbers: [number];
  categories: [Category];
}

Và giá trị lưu vào ref chúng ta nên dùng là _id của quan hệ. Nó giúp tối ưu tốc độ truy vấn và đỡ phải cấu hình thêm.

Về populate căn bản mình đã viết ở phần bài viết

post.populate('user').execPopulate();

chỗ này mình phải dùng hàm execPopulate vì post mà đối tượng mình đã query rồi nên cần gọi hàm này thể thực thi phần populate.

Ngoài ra ta có thể lấy những field cần thiết trong populate. Ví dụ ở đây ta chỉ cần lấy tên người viết thôi, những thông tin khác dư thừa, ta làm như sau:

post.populate({ path: 'user', select: 'name' }).execPopulate();

Hoặc ngược tại, ta không muốn lấy field nào thì thêm dấu trừ vào phía trước, ở đâu ta không muốn lấy email, password chẳng hạn:

post.populate({ path: 'user', select: '-email -password' }).execPopulate();

Còn nếu trong câu truy vấn trực tiếp ta có thể viết như sau:

postModel.findOne(filter, field, option).populate(populate);

những phần này mình viết bên trong BaseRepository các bạn clone source về để dễ theo dõi code của mình.

Lưu ý: Populate hiểu như là left join bên SQL. Nếu không tồn tại quan hệ sẽ trả về null thay vì giá trị hiện có của nó hoặc là 1 mảng rổng nếu field array.

Kiểm tra Populate

Phần này mình hong hiểu là sinh ra để làm gì luôn ý. Có thể mình chưa dùng đến và cũng có thể kiểm tra một số quan hệ có thật sự tồn tại hay không kiểu kiểu thế. Nhưng mình cũng giới thiệu ra cho mọi người cùng biết ạ.

Đầu tiên là check có tồn tại populate hay không, ta sử dụng populated

Giả dụ ở trên ta populate đến thằng User, ta kiểm tra như sau:

console.log(post.populated('user'));

giá trị trả về là id / undefined, rồi bạn thít làm gì thì làm. Chỗ này if else thì nó cũng chạy được nha, hehe

Tiếp theo để hủy việc populate, sau khi populate thì field user là 1 object chứa thông tin của người viết bài đó. Giờ chỉ muốn user là cái id ban đầu mình save lại thôi thì phải làm sao, phải làm sao?

dùng depopulate. Khúc này là hơi cồng kềnh rồi đó, vậy ban đầu populate chi giờ “đì”. Có thì mình giới thiệu cho mọi người biết thoi nè:

post.depopulate('user');

Giả sử như cái đầu trả về true rồi , sau khi chạy lệnh depopulate và chạy lại lệnh populated. Lúc này giá trị nhận được là false nha cả nhà. Và giá trị của user lúc này chỉ có id thôi chứ không phải là object gì hết.

Populate lồng – đa cấp

Trong 1 phần truy vấn, ta có thể populate tới nhiều quan hệ khác nhau.

Có thể populate nhiều lần hoặc populate 1 array các cái path.

await post.populate({ path: 'user' }).populate({ path: 'categories' }).execPopulate();

Từ thằng post ta có thể populate nhiều lần tới thằng User cũng như Category. Ngoài ra ta có thể viết đơn giản hơn bằng cách truyền array vào bên trong populate thay vì phải populate 2 lần.

await post.populate([{ path: 'user' }, { path: 'categories' }]).execPopulate();

Các object bên trong array chúng ta cứ khai báo bình thường như select, match, option ,…

Nâng cao hơn 1 tẹo, trong object mà chúng ta truyền vào populate, mongoose có hỗ trợ cho chúng ta option.

Trong populate chúng ta cũng hoàn toàn có thể populate bên trong nó, chúng ta cùng tìm hiểu:

await post
    .populate({
      path: 'categories',
      match: { _id: '62a45f081fa1129c58dd4201' },
      select: 'title',
      options: { limit: 1 },
      populate: {
         path: 'posts',
      },
    })
    .execPopulate();

Populate ảo

Để làm được điều này, chúng ta cần khai báo field ảo cho model. Field này chúng ta sẽ không lưu bên trong model mà chỉ khai báo phương thức thôi. Ở đây chúng ta sẽ khai báo field ảo virtual bên trong thằng user để lấy những bài viết của user đó. Mở model user lên và thêm đoạn code nào vào:

const ...

UserSchema.virtual('posts', {
  ref: 'Post',
  localField: '_id',
  foreignField: 'user',
  justOne: false,
  // count: true,
  match: {
    categories: { $size: 2 },
  },
});
export { UserSchema };
...

Ở trong controller / service chúng ta sử dụng như sau:

post.populate('posts').execPopulate();

Mình đã để sẵn một số option bên trong field ảo, bạn comment lại từng cái để test hì. Để mình giới thiệu qua những chức năng:

  • ref: như ở trên mình có nói để mongoose hiểu model quan hệ
  • localField: là khóa trên bảng hiện tại dùng để map ( ở đây là bảng user ). Vì ta lưu id của user bên trong thằng post.
  • foreignField: có thể hiểu như khóa ngoại lưu trên model quan hệ ( model post).
  • justOne: trả về 1 đối tượng duy nhất ( trả về 1 object). Nếu comment lại sẽ trả về 1 array các object post.
  • count: nếu dùng nó trả về số lượng bài viết của user đó.
  • match: là điều kiện trên model quan hệ (ở đây là model Post).

Kết luận

Qua bài viết mình đã giới thiệu hầu hết các chức năng của populate mà mongoose hỗ trợ. Nhiêu đây đã đủ để làm những dự án vừa và nhỏ rồi. Tùy vào nhu cầu mà chúng ta áp dụng hợp lý.

Rất mong được sự ủng hộ của mọi người để mình có động lực ra những bài viết tiếp theo.
{\__/}
( ~.~ )
/ > ♥️ I LOVE YOU 3000

JUST DO IT!