Mongodb populate without model parameter

84 Views Asked by At

I am trying to populate some collection in another collection. However without specifying model in populate method it is not replacing with actual document. Here are my code

cabinet.schema.ts

import {Prop, Schema, SchemaFactory} from '@nestjs/mongoose';
import { Date, HydratedDocument, Model, Schema as MongooseSchema } from 'mongoose';

export type CabinetDocument = HydratedDocument<Cabinet>;

export const CabinetProjection = {
    _id: false,
    __v: false,
}

@Schema()
export class Cabinet {

    @Prop({
        required: true,
        unique: true
    })
    id: string;

    @Prop()
    items: [{
        item: { type: MongooseSchema.Types.ObjectId, ref: "Item" },
        status: string;
        amount: number;
    }];
}

export const CabinetSchema = SchemaFactory.createForClass(Cabinet)

item.schema.ts

import {Prop, Schema, SchemaFactory} from '@nestjs/mongoose';
import mongoose, { HydratedDocument } from 'mongoose';

export type ItemDocument = HydratedDocument<Item>;

export const ItemProjection = {
    _id: false,
    __v: false,
}

@Schema()
export class Item {

    @Prop({
        required: true,
        unique: true
    })
    qId: string;

    @Prop()
    name: string;

    @Prop()
    description: string;
}

export const ItemSchema = SchemaFactory.createForClass(Item)

cabinet.service.ts

import { Injectable } from '@nestjs/common';
import { CreateCabinetDto } from './dto/create-cabinet.dto';
import { UpdateCabinetDto } from './dto/update-cabinet.dto';
import { Cabinet, CabinetProjection, CabinetSchema } from './schemas/cabinet.schema';
import { Model } from 'mongoose';
import { InjectModel } from '@nestjs/mongoose';

@Injectable()
export class CabinetService {

  constructor(@InjectModel(Cabinet.name) private cabinetModel: Model<Cabinet>) {}

  async findOne(id: string): Promise<Cabinet> {

    const result = this.cabinetModel.findOne({
      id: id,
    }, CabinetProjection).populate({path: "items.item", model: "Item"}).exec();

    return result;
  }
}

With this setup it works perfectly, but if i remove model: "Item", instead of Item document populated, i see object reference. Isn't ref: "Item" parameter sufficent?

Also my collection names are cabinets and items.

*Edit:

Appearently the problem was in the Cabinet class structure. If i change to below code, it started to work without model parameter. Any explanation would be appreciated.

@Schema()
export class Cabinet {

    @Prop({
        required: true,
        unique: true
    })
    id: string;

    @Prop({ type: [CabinetItem]  })
    items: CabinetItem[];
}

@Schema()
export class CabinetItem{

    @Prop({ type: MongooseSchema.Types.ObjectId, ref: Item.name })
    item: Item,
    
    @Prop()
    status: string;
    
    @Prop()
    amount: number;
}
1

There are 1 best solutions below

6
jQueeny On

This is one of the biggest sources of confusion when I'm working with developers using mongoose. Hopefully this will explain.

When I set up a ref on a schema property I need to specify the model that I want mongoose to use to do the populate and replace all of my ObjectId with the actual documents from the referenced collection.

Here is a simple example of users that listen to songs:

// User.js
import mongoose from 'mongoose';
const userSchema = new mongoose.Schema({
    name: String,
    songs: [{
        type: mongoose.Schema.Types.ObjectId,
        ref: 'Song'
    }]
})
const User = mongoose.model("User", userSchema);
export default User;
// Song.js
import mongoose from 'mongoose';
const songSchema = new mongoose.Schema({
    title: String,
    artist: String
})
const Song = mongoose.model("Song", songSchema);
export default Song;
import mongoose from 'mongoose';
import User from "../models/User.js";
import Song from "../models/Song.js";
mongoose.connect('mongodb://127.0.0.1:27017/myapp').then(()=>{
   User.find().populate('songs').then(users =>{
      console.log(users);
   });
});

  1. Mongoose maps model names to collections names. That means when I create the model, mongoose looks for the lowercase, pluralised version of first argument in that function:
mongoose.model("User", userSchema);
// "User" model --> "users" collection
mongoose.model("Song", songSchema);
// "Song" model --> "songs" collection
  1. Mongoose now has 2 models registered that it knows about:
console.log(mongoose.connection.models);
{
  User: Model { User },
  Song: Model { Song },
}
  1. When I try to populate all the User.songs mongoose will first look at the options object of the populate method for the model parameter. If it's not there the ref value from the schema will be used to look for which model to use.
User.find().populate('songs');
// You want to populate 'songs' huh? 
// There is no options object such as {path: 'songs', model: 'Song'}
// Let me look at your schema for 'songs':
songs: [{
   type: mongoose.Schema.Types.ObjectId,
   ref: 'Song' //< Bingo
}]
// I have found the model you want me to use. It's 'Song', thank you!
  1. Mongoose will now look in mongoose.connection.models to find that Song model and it can now use it to do the populate. Under the hood it uses functions called modelNamesFromRefPath and getModelsMapForPopulate.
  2. The key here is the connections. Different connections can have different models registered. For the above populate to work both User and Song models would need to be registered on the same connection. In my case I used the default mongoose.connect connection so both my models are registered on mongoose.connection.models. However, some libraries use mongoose.createConnection and register one model on one connection and another model on a different connection. In that scenario you would have to pass the actual model and not just the name of the model as a parameter to populate like so:
User.find().populate({path:'songs', model: Song});

So to answer your question "Isn't ref: "Item" parameter sufficient?", yes it is sufficient, normally. It may be that the Item model is not in scope within that class when you run populate or it's the Nest.js library that needs the model name as part of it's own model/connection management at that stage in your design. You should try to import the item.schema into the cabinet.schema to see if you still need to explicity state Item in the populate.