猫鼬用吸气剂功能填充虚拟字段不起作用 [英] Mongoose populate virtual field with a getter function is not working

查看:72
本文介绍了猫鼬用吸气剂功能填充虚拟字段不起作用的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在用吸气剂功能填充猫鼬模式中的虚拟字段. 它应该填充该字段而没有任何错误,但是会引发错误

MongooseError:如果要填充虚拟机,则必须设置localField 和foreignField选项

当我们使用getter填充字段时,则不需要localfield和foriegn字段

const DashboardSchema = new Schema({
    userId: {
        type: SchemaTypes.ObjectId
    }
}, { toJSON: { virtuals: true } });

DashboardSchema.virtual('TopReports').get(function () {
   return TopReports.find({ userId: this.userId }).sort('-date').limit(10);
})

解决方案

如果要使用sortlimit进行虚拟填充",那实际上不是您的操作方式.您创建的只是一个虚拟获取器",实际上是在返回异步函数的结果.您可以使用它,但是要解决返回的Promise则要麻烦得多,并且与populate()毫无关系,这是您提出错误的地方.

执行此操作还有其他选择.

猫鼬虚拟填充

为此,它最接近您的尝试,相反,您需要这样的东西:

const dashboardSchema = new Schema({
  userId: Schema.Types.ObjectId
},
{
  toJSON: { virtuals: true },
  toObject: { virtuals: true }
});

dashboardSchema.virtual('TopReports', {
  ref: 'Report',
  localField: 'userId',
  foreignField: 'userId',
  options: { sort: { date: -1 }, limit: 10 } // optional - could add with populate
});

const reportSchema = new Schema({
  userId: Schema.Types.ObjectId,
  seq: Number,
  date: Date
});

const Dashboard = mongoose.model('Dashboard', dashboardSchema);
const Report = mongoose.model('Report', reportSchema);

这实际上与人口虚拟对象" ,因为给定示例中还包含options,这与将选项传递给populate方法本身相同.在virtual中设置options时,您只需要这样调用:

let result = await Dashboard.findOne().populate('TopReports');

在执行populate()时,为sortlimit设置的默认值会自动应用到此虚拟"字段.如果选择不包括options,则只需手动添加选项:

let result2 = await Dashboard.findOne().populate({
  path: 'TopReports',
  options: { sort: '-date', limit: 5 }
});
log(result2);

重要-在virtual中设置options将会始终覆盖传递给populate()的任何options,如上所示.如果要对不同的请求使用不同的options,则可以按上述方法调用,而不要在附加到架构的virtual方法上进行定义.

这就是您真正需要做的.当然,定义包括localFieldforeignField以及ref所有,因此populate()调用知道从何处获取数据以及与哪些字段相关.还有一个可选 justOne可以区分单数结果和Array结果,还有一些其他选择.

MongoDB $ lookup

这里的另一种选择是MongoDB基本上具有相同的内置功能,不同的是单个请求,而不是populate()实际上是多个请求以便从单独的集合中返回数据:

   let result = await Dashboard.aggregate([
      { "$lookup": {
        "from": Report.collection.name,
        "let": { "userId": "$userId" },
        "pipeline": [
          { "$match": { "$expr": { "$eq": [ "$userId", "$$userId" ] } } },
          { "$sort": { "date": -1 } },
          { "$limit": 10 }
        ],
        "as": "TopReports"
      }}
    ]);

因此,与populate()实际发出的许多" find()请求相比,这是一个具有单个响应的请求.结果相同,只是使用了 ,以应用 $sort $limit 到返回的相关项目数组.

只需做一些工作,您甚至可以从猫鼬模式中检查模式定义(包括已定义的虚拟),并构造相同的在猫鼬中填充后进行查询,对此模式检查"进行了基本演示.


因此,这取决于最适合您的需求.我建议同时尝试甚至基准测试应用程序性能.

作为完整的演示,这是示例清单.它插入20个东西,并只返回数组中最近"的10个结果:

const { Schema, Types: { ObjectId } } = mongoose = require('mongoose');

const uri = 'mongodb://localhost:27017/test';
const opts = { useNewUrlParser: true };

mongoose.set('useFindAndModify', false);
mongoose.set('useCreateIndex', true);
mongoose.set('debug', true);

const dashboardSchema = new Schema({
  userId: Schema.Types.ObjectId
},
{
  toJSON: { virtuals: true },
  toObject: { virtuals: true }
});

dashboardSchema.virtual('TopReports', {
  ref: 'Report',
  localField: 'userId',
  foreignField: 'userId',
  options: { sort: { date: -1 }, limit: 10 } // optional - could add with populate
});

const reportSchema = new Schema({
  userId: Schema.Types.ObjectId,
  seq: Number,
  date: Date
});

const Dashboard = mongoose.model('Dashboard', dashboardSchema);
const Report = mongoose.model('Report', reportSchema);

const log = data => console.log(JSON.stringify(data, undefined, 2));

(async function() {

  try {

    const conn = await mongoose.connect(uri, opts);

    await Promise.all(
      Object.entries(conn.models).map(([k,m]) => m.deleteMany())
    );

    // Insert some things
    let { userId } = await Dashboard.create({ userId: new ObjectId() });

    const oneDay = ( 1000 * 60 * 60 * 24 );
    const baseDate = Date.now() - (oneDay * 30); // 30 days ago

    await Report.insertMany(
      [ ...Array(20)]
        .map((e,i) => ({ userId, seq: i+1, date: baseDate + ( oneDay * i ) }))
    );

    // Virtual populate
    let popresult = await Dashboard.findOne().populate('TopReports');
    log(popresult);


    // Aggregate $lookup
    let result = await Dashboard.aggregate([
      { "$lookup": {
        "from": Report.collection.name,
        "let": { "userId": "$userId" },
        "pipeline": [
          { "$match": { "$expr": { "$eq": [ "$userId", "$$userId" ] } } },
          { "$sort": { "date": -1 } },
          { "$limit": 10 }
        ],
        "as": "TopReports"
      }}
    ]);

    log(result);


  } catch (e) {
    console.error(e)
  } finally {
    mongoose.disconnect()
  }

})()

输出:

Mongoose: dashboards.deleteMany({}, {})
Mongoose: reports.deleteMany({}, {})
Mongoose: dashboards.insertOne({ _id: ObjectId("5cce3d9e16302f32acb5c572"), userId: ObjectId("5cce3d9e16302f32acb5c571"), __v: 0 })
Mongoose: reports.insertMany([ { _id: 5cce3d9e16302f32acb5c573, userId: 5cce3d9e16302f32acb5c571, seq: 1, date: 2019-04-05T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c574, userId: 5cce3d9e16302f32acb5c571, seq: 2, date: 2019-04-06T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c575, userId: 5cce3d9e16302f32acb5c571, seq: 3, date: 2019-04-07T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c576, userId: 5cce3d9e16302f32acb5c571, seq: 4, date: 2019-04-08T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c577, userId: 5cce3d9e16302f32acb5c571, seq: 5, date: 2019-04-09T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c578, userId: 5cce3d9e16302f32acb5c571, seq: 6, date: 2019-04-10T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c579, userId: 5cce3d9e16302f32acb5c571, seq: 7, date: 2019-04-11T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c57a, userId: 5cce3d9e16302f32acb5c571, seq: 8, date: 2019-04-12T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c57b, userId: 5cce3d9e16302f32acb5c571, seq: 9, date: 2019-04-13T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c57c, userId: 5cce3d9e16302f32acb5c571, seq: 10, date: 2019-04-14T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c57d, userId: 5cce3d9e16302f32acb5c571, seq: 11, date: 2019-04-15T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c57e, userId: 5cce3d9e16302f32acb5c571, seq: 12, date: 2019-04-16T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c57f, userId: 5cce3d9e16302f32acb5c571, seq: 13, date: 2019-04-17T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c580, userId: 5cce3d9e16302f32acb5c571, seq: 14, date: 2019-04-18T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c581, userId: 5cce3d9e16302f32acb5c571, seq: 15, date: 2019-04-19T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c582, userId: 5cce3d9e16302f32acb5c571, seq: 16, date: 2019-04-20T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c583, userId: 5cce3d9e16302f32acb5c571, seq: 17, date: 2019-04-21T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c584, userId: 5cce3d9e16302f32acb5c571, seq: 18, date: 2019-04-22T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c585, userId: 5cce3d9e16302f32acb5c571, seq: 19, date: 2019-04-23T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c586, userId: 5cce3d9e16302f32acb5c571, seq: 20, date: 2019-04-24T01:34:22.554Z, __v: 0 } ], {})
Mongoose: dashboards.findOne({}, { projection: {} })
Mongoose: reports.find({ userId: { '$in': [ ObjectId("5cce3d9e16302f32acb5c571") ] } }, { sort: { date: -1 }, limit: 10, projection: {} })
{
  "_id": "5cce3d9e16302f32acb5c572",
  "userId": "5cce3d9e16302f32acb5c571",
  "__v": 0,
  "TopReports": [
    {
      "_id": "5cce3d9e16302f32acb5c586",
      "userId": "5cce3d9e16302f32acb5c571",
      "seq": 20,
      "date": "2019-04-24T01:34:22.554Z",
      "__v": 0
    },
    {
      "_id": "5cce3d9e16302f32acb5c585",
      "userId": "5cce3d9e16302f32acb5c571",
      "seq": 19,
      "date": "2019-04-23T01:34:22.554Z",
      "__v": 0
    },
    {
      "_id": "5cce3d9e16302f32acb5c584",
      "userId": "5cce3d9e16302f32acb5c571",
      "seq": 18,
      "date": "2019-04-22T01:34:22.554Z",
      "__v": 0
    },
    {
      "_id": "5cce3d9e16302f32acb5c583",
      "userId": "5cce3d9e16302f32acb5c571",
      "seq": 17,
      "date": "2019-04-21T01:34:22.554Z",
      "__v": 0
    },
    {
      "_id": "5cce3d9e16302f32acb5c582",
      "userId": "5cce3d9e16302f32acb5c571",
      "seq": 16,
      "date": "2019-04-20T01:34:22.554Z",
      "__v": 0
    },
    {
      "_id": "5cce3d9e16302f32acb5c581",
      "userId": "5cce3d9e16302f32acb5c571",
      "seq": 15,
      "date": "2019-04-19T01:34:22.554Z",
      "__v": 0
    },
    {
      "_id": "5cce3d9e16302f32acb5c580",
      "userId": "5cce3d9e16302f32acb5c571",
      "seq": 14,
      "date": "2019-04-18T01:34:22.554Z",
      "__v": 0
    },
    {
      "_id": "5cce3d9e16302f32acb5c57f",
      "userId": "5cce3d9e16302f32acb5c571",
      "seq": 13,
      "date": "2019-04-17T01:34:22.554Z",
      "__v": 0
    },
    {
      "_id": "5cce3d9e16302f32acb5c57e",
      "userId": "5cce3d9e16302f32acb5c571",
      "seq": 12,
      "date": "2019-04-16T01:34:22.554Z",
      "__v": 0
    },
    {
      "_id": "5cce3d9e16302f32acb5c57d",
      "userId": "5cce3d9e16302f32acb5c571",
      "seq": 11,
      "date": "2019-04-15T01:34:22.554Z",
      "__v": 0
    }
  ],
  "id": "5cce3d9e16302f32acb5c572"
}
Mongoose: dashboards.aggregate([ { '$lookup': { from: 'reports', let: { userId: '$userId' }, pipeline: [ { '$match': { '$expr': { '$eq': [ '$userId', '$$userId' ] } } }, { '$sort': { date: -1 } }, { '$limit': 10 } ], as: 'TopReports' } } ], {})
[
  {
    "_id": "5cce3d9e16302f32acb5c572",
    "userId": "5cce3d9e16302f32acb5c571",
    "__v": 0,
    "TopReports": [
      {
        "_id": "5cce3d9e16302f32acb5c586",
        "userId": "5cce3d9e16302f32acb5c571",
        "seq": 20,
        "date": "2019-04-24T01:34:22.554Z",
        "__v": 0
      },
      {
        "_id": "5cce3d9e16302f32acb5c585",
        "userId": "5cce3d9e16302f32acb5c571",
        "seq": 19,
        "date": "2019-04-23T01:34:22.554Z",
        "__v": 0
      },
      {
        "_id": "5cce3d9e16302f32acb5c584",
        "userId": "5cce3d9e16302f32acb5c571",
        "seq": 18,
        "date": "2019-04-22T01:34:22.554Z",
        "__v": 0
      },
      {
        "_id": "5cce3d9e16302f32acb5c583",
        "userId": "5cce3d9e16302f32acb5c571",
        "seq": 17,
        "date": "2019-04-21T01:34:22.554Z",
        "__v": 0
      },
      {
        "_id": "5cce3d9e16302f32acb5c582",
        "userId": "5cce3d9e16302f32acb5c571",
        "seq": 16,
        "date": "2019-04-20T01:34:22.554Z",
        "__v": 0
      },
      {
        "_id": "5cce3d9e16302f32acb5c581",
        "userId": "5cce3d9e16302f32acb5c571",
        "seq": 15,
        "date": "2019-04-19T01:34:22.554Z",
        "__v": 0
      },
      {
        "_id": "5cce3d9e16302f32acb5c580",
        "userId": "5cce3d9e16302f32acb5c571",
        "seq": 14,
        "date": "2019-04-18T01:34:22.554Z",
        "__v": 0
      },
      {
        "_id": "5cce3d9e16302f32acb5c57f",
        "userId": "5cce3d9e16302f32acb5c571",
        "seq": 13,
        "date": "2019-04-17T01:34:22.554Z",
        "__v": 0
      },
      {
        "_id": "5cce3d9e16302f32acb5c57e",
        "userId": "5cce3d9e16302f32acb5c571",
        "seq": 12,
        "date": "2019-04-16T01:34:22.554Z",
        "__v": 0
      },
      {
        "_id": "5cce3d9e16302f32acb5c57d",
        "userId": "5cce3d9e16302f32acb5c571",
        "seq": 11,
        "date": "2019-04-15T01:34:22.554Z",
        "__v": 0
      }
    ]
  }
]


错误的方式

只是为了演示"getter"方法有什么问题,下面的示例清单显示了在每个返回的对象上实际解决返回的Promise的情况:

const { Schema, Types: { ObjectId } } = mongoose = require('mongoose');

const uri = 'mongodb://localhost:27017/test';
const opts = { useNewUrlParser: true };

mongoose.set('useFindAndModify', false);
mongoose.set('useCreateIndex', true);
mongoose.set('debug', true);

const dashboardSchema = new Schema({
  userId: Schema.Types.ObjectId
},
{
  toJSON: { virtuals: true },
  toObject: { virtuals: true }
});

dashboardSchema.virtual('TopReports').get(function() {
  return Report.find({ userId: this.userId }).sort("-date").limit(10);
});

const reportSchema = new Schema({
  userId: Schema.Types.ObjectId,
  seq: Number,
  date: Date
});

const Dashboard = mongoose.model('Dashboard', dashboardSchema);
const Report = mongoose.model('Report', reportSchema);

const log = data => console.log(JSON.stringify(data, undefined, 2));

(async function() {

  try {

    const conn = await mongoose.connect(uri, opts);

    await Promise.all(
      Object.entries(conn.models).map(([k,m]) => m.deleteMany())
    );

    // Insert some things
    let { userId } = await Dashboard.create({ userId: new ObjectId() });

    const oneDay = ( 1000 * 60 * 60 * 24 );
    const baseDate = Date.now() - (oneDay * 30); // 30 days ago

    await Report.insertMany(
      [ ...Array(20)]
        .map((e,i) => ({ userId, seq: i+1, date: baseDate + ( oneDay * i ) }))
    );

    // Mimic the virtual populate with the getter
    let results = await Dashboard.find();
    for ( let r of results ) {
      let obj = { ...r.toObject() };        // copy the plain object data only
      obj.TopReports = await r.TopReports;  // Resolve the Promise
      log(obj);
    }

  } catch (e) {
    console.error(e)
  } finally {
    mongoose.disconnect()
  }

})()

并输出:

Mongoose: dashboards.deleteMany({}, {})
Mongoose: reports.deleteMany({}, {})
Mongoose: dashboards.insertOne({ _id: ObjectId("5cce45193134aa37e88c4114"), userId: ObjectId("5cce45193134aa37e88c4113"), __v: 0 })
Mongoose: reports.insertMany([ { _id: 5cce45193134aa37e88c4115, userId: 5cce45193134aa37e88c4113, seq: 1, date: 2019-04-05T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4116, userId: 5cce45193134aa37e88c4113, seq: 2, date: 2019-04-06T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4117, userId: 5cce45193134aa37e88c4113, seq: 3, date: 2019-04-07T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4118, userId: 5cce45193134aa37e88c4113, seq: 4, date: 2019-04-08T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4119, userId: 5cce45193134aa37e88c4113, seq: 5, date: 2019-04-09T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c411a, userId: 5cce45193134aa37e88c4113, seq: 6, date: 2019-04-10T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c411b, userId: 5cce45193134aa37e88c4113, seq: 7, date: 2019-04-11T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c411c, userId: 5cce45193134aa37e88c4113, seq: 8, date: 2019-04-12T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c411d, userId: 5cce45193134aa37e88c4113, seq: 9, date: 2019-04-13T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c411e, userId: 5cce45193134aa37e88c4113, seq: 10, date: 2019-04-14T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c411f, userId: 5cce45193134aa37e88c4113, seq: 11, date: 2019-04-15T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4120, userId: 5cce45193134aa37e88c4113, seq: 12, date: 2019-04-16T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4121, userId: 5cce45193134aa37e88c4113, seq: 13, date: 2019-04-17T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4122, userId: 5cce45193134aa37e88c4113, seq: 14, date: 2019-04-18T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4123, userId: 5cce45193134aa37e88c4113, seq: 15, date: 2019-04-19T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4124, userId: 5cce45193134aa37e88c4113, seq: 16, date: 2019-04-20T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4125, userId: 5cce45193134aa37e88c4113, seq: 17, date: 2019-04-21T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4126, userId: 5cce45193134aa37e88c4113, seq: 18, date: 2019-04-22T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4127, userId: 5cce45193134aa37e88c4113, seq: 19, date: 2019-04-23T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4128, userId: 5cce45193134aa37e88c4113, seq: 20, date: 2019-04-24T02:06:17.518Z, __v: 0 } ], {})
Mongoose: dashboards.find({}, { projection: {} })
Mongoose: reports.find({ userId: ObjectId("5cce45193134aa37e88c4113") }, { sort: { date: -1 }, limit: 10, projection: {} })
{
  "_id": "5cce45193134aa37e88c4114",
  "userId": "5cce45193134aa37e88c4113",
  "__v": 0,
  "TopReports": [
    {
      "_id": "5cce45193134aa37e88c4128",
      "userId": "5cce45193134aa37e88c4113",
      "seq": 20,
      "date": "2019-04-24T02:06:17.518Z",
      "__v": 0
    },
    {
      "_id": "5cce45193134aa37e88c4127",
      "userId": "5cce45193134aa37e88c4113",
      "seq": 19,
      "date": "2019-04-23T02:06:17.518Z",
      "__v": 0
    },
    {
      "_id": "5cce45193134aa37e88c4126",
      "userId": "5cce45193134aa37e88c4113",
      "seq": 18,
      "date": "2019-04-22T02:06:17.518Z",
      "__v": 0
    },
    {
      "_id": "5cce45193134aa37e88c4125",
      "userId": "5cce45193134aa37e88c4113",
      "seq": 17,
      "date": "2019-04-21T02:06:17.518Z",
      "__v": 0
    },
    {
      "_id": "5cce45193134aa37e88c4124",
      "userId": "5cce45193134aa37e88c4113",
      "seq": 16,
      "date": "2019-04-20T02:06:17.518Z",
      "__v": 0
    },
    {
      "_id": "5cce45193134aa37e88c4123",
      "userId": "5cce45193134aa37e88c4113",
      "seq": 15,
      "date": "2019-04-19T02:06:17.518Z",
      "__v": 0
    },
    {
      "_id": "5cce45193134aa37e88c4122",
      "userId": "5cce45193134aa37e88c4113",
      "seq": 14,
      "date": "2019-04-18T02:06:17.518Z",
      "__v": 0
    },
    {
      "_id": "5cce45193134aa37e88c4121",
      "userId": "5cce45193134aa37e88c4113",
      "seq": 13,
      "date": "2019-04-17T02:06:17.518Z",
      "__v": 0
    },
    {
      "_id": "5cce45193134aa37e88c4120",
      "userId": "5cce45193134aa37e88c4113",
      "seq": 12,
      "date": "2019-04-16T02:06:17.518Z",
      "__v": 0
    },
    {
      "_id": "5cce45193134aa37e88c411f",
      "userId": "5cce45193134aa37e88c4113",
      "seq": 11,
      "date": "2019-04-15T02:06:17.518Z",
      "__v": 0
    }
  ],
  "id": "5cce45193134aa37e88c4114"
}

当然,对于每个返回的文档,都需要解析getter返回的Promise.相比之下,populate()使用$in来通过单个请求返回find()中所有结果的匹配条目,这将为每个Dashboard文档而不是单个find()发出一个新的find(). c24>基于结果中每个Dashboard文档中找到的所有userId值.

基本上,与populate()$lookup相对,您实际上是在将"join"逻辑拆分成控制流的一部分,该部分实际上不属于该流,并且变得难以管理以及生成更多内容请求返回服务器.

i am populating a virtual field in mongoose schema with a getter function. It should populate the field without any errors but it is throwing error

MongooseError: If you are populating a virtual, you must set the localField and foreignField options

when we are populating a field with getter then the localfield and foriegn field is not required

const DashboardSchema = new Schema({
    userId: {
        type: SchemaTypes.ObjectId
    }
}, { toJSON: { virtuals: true } });

DashboardSchema.virtual('TopReports').get(function () {
   return TopReports.find({ userId: this.userId }).sort('-date').limit(10);
})

解决方案

If you want a "virtual populate" with a sort and limit then that's not actually how you do it. What you created is just a "virtual getter" which is actually returning the result of an async function. You can use that, but it's a lot trickier to manage resolving the returned Promise and it really has nothing to do with populate(), which is where you are raising an error.

There are also different options to doing this.

Mongoose Virtual Populate

For this which is closest to what you attempted, Instead you want something like this:

const dashboardSchema = new Schema({
  userId: Schema.Types.ObjectId
},
{
  toJSON: { virtuals: true },
  toObject: { virtuals: true }
});

dashboardSchema.virtual('TopReports', {
  ref: 'Report',
  localField: 'userId',
  foreignField: 'userId',
  options: { sort: { date: -1 }, limit: 10 } // optional - could add with populate
});

const reportSchema = new Schema({
  userId: Schema.Types.ObjectId,
  seq: Number,
  date: Date
});

const Dashboard = mongoose.model('Dashboard', dashboardSchema);
const Report = mongoose.model('Report', reportSchema);

This is actually almost identical to the documentation example on "Populate Virtuals" in that the given example there also includes the options which are the same as passing options to the populate method itself. When you set options in the virtual you only need invoke like this:

let result = await Dashboard.findOne().populate('TopReports');

The defaults set for sort and limit are automatically applied on this "virtual" field as the populate() is performed. If you chose NOT to include the options you would just add the options manually:

let result2 = await Dashboard.findOne().populate({
  path: 'TopReports',
  options: { sort: '-date', limit: 5 }
});
log(result2);

IMPORTANT - Setting the options within the virtual will ALWAYS OVERRIDE any options passed to populate() as shown above. If you want to use different options on different requests, then you call on the method as above rather than defining on the virtual method attached to the schema.

That's all you really need do. The definition of course includes localField and foreignField as well as the ref all so the populate() call knows where to get the data from and which fields to relate to. There is also an optional justOne which differentiates between singular and Array results, as well as a few other options.

MongoDB $lookup

The other option here is that MongoDB basically has the same functionality built in anyway, with the exception that this is single request as opposed to populate() which is actually multiple requests in order to return the data from separate collections:

   let result = await Dashboard.aggregate([
      { "$lookup": {
        "from": Report.collection.name,
        "let": { "userId": "$userId" },
        "pipeline": [
          { "$match": { "$expr": { "$eq": [ "$userId", "$$userId" ] } } },
          { "$sort": { "date": -1 } },
          { "$limit": 10 }
        ],
        "as": "TopReports"
      }}
    ]);

So that's one request with a single response as compared to the "many" find() requests actually issued by populate(). It's the same result and just used the "sub-pipeline" form of $lookup in order to apply the $sort and $limit to the returned array of related items.

With a little work, you can even inspect the schema definition ( including a defined virtual ) from the mongoose schema and construct the same $lookup statement. There is a basic demonstration of this "schema inspection" on Querying after populate in Mongoose.


So it depends on which suits your needs best. I suggest trying both and even bench-marking application performance.

As a full demonstration, here is an example listing. It inserts 20 things and just returns the most "recent" 10 results in the array:

const { Schema, Types: { ObjectId } } = mongoose = require('mongoose');

const uri = 'mongodb://localhost:27017/test';
const opts = { useNewUrlParser: true };

mongoose.set('useFindAndModify', false);
mongoose.set('useCreateIndex', true);
mongoose.set('debug', true);

const dashboardSchema = new Schema({
  userId: Schema.Types.ObjectId
},
{
  toJSON: { virtuals: true },
  toObject: { virtuals: true }
});

dashboardSchema.virtual('TopReports', {
  ref: 'Report',
  localField: 'userId',
  foreignField: 'userId',
  options: { sort: { date: -1 }, limit: 10 } // optional - could add with populate
});

const reportSchema = new Schema({
  userId: Schema.Types.ObjectId,
  seq: Number,
  date: Date
});

const Dashboard = mongoose.model('Dashboard', dashboardSchema);
const Report = mongoose.model('Report', reportSchema);

const log = data => console.log(JSON.stringify(data, undefined, 2));

(async function() {

  try {

    const conn = await mongoose.connect(uri, opts);

    await Promise.all(
      Object.entries(conn.models).map(([k,m]) => m.deleteMany())
    );

    // Insert some things
    let { userId } = await Dashboard.create({ userId: new ObjectId() });

    const oneDay = ( 1000 * 60 * 60 * 24 );
    const baseDate = Date.now() - (oneDay * 30); // 30 days ago

    await Report.insertMany(
      [ ...Array(20)]
        .map((e,i) => ({ userId, seq: i+1, date: baseDate + ( oneDay * i ) }))
    );

    // Virtual populate
    let popresult = await Dashboard.findOne().populate('TopReports');
    log(popresult);


    // Aggregate $lookup
    let result = await Dashboard.aggregate([
      { "$lookup": {
        "from": Report.collection.name,
        "let": { "userId": "$userId" },
        "pipeline": [
          { "$match": { "$expr": { "$eq": [ "$userId", "$$userId" ] } } },
          { "$sort": { "date": -1 } },
          { "$limit": 10 }
        ],
        "as": "TopReports"
      }}
    ]);

    log(result);


  } catch (e) {
    console.error(e)
  } finally {
    mongoose.disconnect()
  }

})()

And the output:

Mongoose: dashboards.deleteMany({}, {})
Mongoose: reports.deleteMany({}, {})
Mongoose: dashboards.insertOne({ _id: ObjectId("5cce3d9e16302f32acb5c572"), userId: ObjectId("5cce3d9e16302f32acb5c571"), __v: 0 })
Mongoose: reports.insertMany([ { _id: 5cce3d9e16302f32acb5c573, userId: 5cce3d9e16302f32acb5c571, seq: 1, date: 2019-04-05T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c574, userId: 5cce3d9e16302f32acb5c571, seq: 2, date: 2019-04-06T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c575, userId: 5cce3d9e16302f32acb5c571, seq: 3, date: 2019-04-07T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c576, userId: 5cce3d9e16302f32acb5c571, seq: 4, date: 2019-04-08T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c577, userId: 5cce3d9e16302f32acb5c571, seq: 5, date: 2019-04-09T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c578, userId: 5cce3d9e16302f32acb5c571, seq: 6, date: 2019-04-10T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c579, userId: 5cce3d9e16302f32acb5c571, seq: 7, date: 2019-04-11T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c57a, userId: 5cce3d9e16302f32acb5c571, seq: 8, date: 2019-04-12T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c57b, userId: 5cce3d9e16302f32acb5c571, seq: 9, date: 2019-04-13T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c57c, userId: 5cce3d9e16302f32acb5c571, seq: 10, date: 2019-04-14T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c57d, userId: 5cce3d9e16302f32acb5c571, seq: 11, date: 2019-04-15T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c57e, userId: 5cce3d9e16302f32acb5c571, seq: 12, date: 2019-04-16T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c57f, userId: 5cce3d9e16302f32acb5c571, seq: 13, date: 2019-04-17T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c580, userId: 5cce3d9e16302f32acb5c571, seq: 14, date: 2019-04-18T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c581, userId: 5cce3d9e16302f32acb5c571, seq: 15, date: 2019-04-19T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c582, userId: 5cce3d9e16302f32acb5c571, seq: 16, date: 2019-04-20T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c583, userId: 5cce3d9e16302f32acb5c571, seq: 17, date: 2019-04-21T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c584, userId: 5cce3d9e16302f32acb5c571, seq: 18, date: 2019-04-22T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c585, userId: 5cce3d9e16302f32acb5c571, seq: 19, date: 2019-04-23T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c586, userId: 5cce3d9e16302f32acb5c571, seq: 20, date: 2019-04-24T01:34:22.554Z, __v: 0 } ], {})
Mongoose: dashboards.findOne({}, { projection: {} })
Mongoose: reports.find({ userId: { '$in': [ ObjectId("5cce3d9e16302f32acb5c571") ] } }, { sort: { date: -1 }, limit: 10, projection: {} })
{
  "_id": "5cce3d9e16302f32acb5c572",
  "userId": "5cce3d9e16302f32acb5c571",
  "__v": 0,
  "TopReports": [
    {
      "_id": "5cce3d9e16302f32acb5c586",
      "userId": "5cce3d9e16302f32acb5c571",
      "seq": 20,
      "date": "2019-04-24T01:34:22.554Z",
      "__v": 0
    },
    {
      "_id": "5cce3d9e16302f32acb5c585",
      "userId": "5cce3d9e16302f32acb5c571",
      "seq": 19,
      "date": "2019-04-23T01:34:22.554Z",
      "__v": 0
    },
    {
      "_id": "5cce3d9e16302f32acb5c584",
      "userId": "5cce3d9e16302f32acb5c571",
      "seq": 18,
      "date": "2019-04-22T01:34:22.554Z",
      "__v": 0
    },
    {
      "_id": "5cce3d9e16302f32acb5c583",
      "userId": "5cce3d9e16302f32acb5c571",
      "seq": 17,
      "date": "2019-04-21T01:34:22.554Z",
      "__v": 0
    },
    {
      "_id": "5cce3d9e16302f32acb5c582",
      "userId": "5cce3d9e16302f32acb5c571",
      "seq": 16,
      "date": "2019-04-20T01:34:22.554Z",
      "__v": 0
    },
    {
      "_id": "5cce3d9e16302f32acb5c581",
      "userId": "5cce3d9e16302f32acb5c571",
      "seq": 15,
      "date": "2019-04-19T01:34:22.554Z",
      "__v": 0
    },
    {
      "_id": "5cce3d9e16302f32acb5c580",
      "userId": "5cce3d9e16302f32acb5c571",
      "seq": 14,
      "date": "2019-04-18T01:34:22.554Z",
      "__v": 0
    },
    {
      "_id": "5cce3d9e16302f32acb5c57f",
      "userId": "5cce3d9e16302f32acb5c571",
      "seq": 13,
      "date": "2019-04-17T01:34:22.554Z",
      "__v": 0
    },
    {
      "_id": "5cce3d9e16302f32acb5c57e",
      "userId": "5cce3d9e16302f32acb5c571",
      "seq": 12,
      "date": "2019-04-16T01:34:22.554Z",
      "__v": 0
    },
    {
      "_id": "5cce3d9e16302f32acb5c57d",
      "userId": "5cce3d9e16302f32acb5c571",
      "seq": 11,
      "date": "2019-04-15T01:34:22.554Z",
      "__v": 0
    }
  ],
  "id": "5cce3d9e16302f32acb5c572"
}
Mongoose: dashboards.aggregate([ { '$lookup': { from: 'reports', let: { userId: '$userId' }, pipeline: [ { '$match': { '$expr': { '$eq': [ '$userId', '$$userId' ] } } }, { '$sort': { date: -1 } }, { '$limit': 10 } ], as: 'TopReports' } } ], {})
[
  {
    "_id": "5cce3d9e16302f32acb5c572",
    "userId": "5cce3d9e16302f32acb5c571",
    "__v": 0,
    "TopReports": [
      {
        "_id": "5cce3d9e16302f32acb5c586",
        "userId": "5cce3d9e16302f32acb5c571",
        "seq": 20,
        "date": "2019-04-24T01:34:22.554Z",
        "__v": 0
      },
      {
        "_id": "5cce3d9e16302f32acb5c585",
        "userId": "5cce3d9e16302f32acb5c571",
        "seq": 19,
        "date": "2019-04-23T01:34:22.554Z",
        "__v": 0
      },
      {
        "_id": "5cce3d9e16302f32acb5c584",
        "userId": "5cce3d9e16302f32acb5c571",
        "seq": 18,
        "date": "2019-04-22T01:34:22.554Z",
        "__v": 0
      },
      {
        "_id": "5cce3d9e16302f32acb5c583",
        "userId": "5cce3d9e16302f32acb5c571",
        "seq": 17,
        "date": "2019-04-21T01:34:22.554Z",
        "__v": 0
      },
      {
        "_id": "5cce3d9e16302f32acb5c582",
        "userId": "5cce3d9e16302f32acb5c571",
        "seq": 16,
        "date": "2019-04-20T01:34:22.554Z",
        "__v": 0
      },
      {
        "_id": "5cce3d9e16302f32acb5c581",
        "userId": "5cce3d9e16302f32acb5c571",
        "seq": 15,
        "date": "2019-04-19T01:34:22.554Z",
        "__v": 0
      },
      {
        "_id": "5cce3d9e16302f32acb5c580",
        "userId": "5cce3d9e16302f32acb5c571",
        "seq": 14,
        "date": "2019-04-18T01:34:22.554Z",
        "__v": 0
      },
      {
        "_id": "5cce3d9e16302f32acb5c57f",
        "userId": "5cce3d9e16302f32acb5c571",
        "seq": 13,
        "date": "2019-04-17T01:34:22.554Z",
        "__v": 0
      },
      {
        "_id": "5cce3d9e16302f32acb5c57e",
        "userId": "5cce3d9e16302f32acb5c571",
        "seq": 12,
        "date": "2019-04-16T01:34:22.554Z",
        "__v": 0
      },
      {
        "_id": "5cce3d9e16302f32acb5c57d",
        "userId": "5cce3d9e16302f32acb5c571",
        "seq": 11,
        "date": "2019-04-15T01:34:22.554Z",
        "__v": 0
      }
    ]
  }
]


The Wrong Way

Just to demonstrate what is wrong with the "getter" approach, here is an example listing showing actually resolving the returned Promise on each returned object:

const { Schema, Types: { ObjectId } } = mongoose = require('mongoose');

const uri = 'mongodb://localhost:27017/test';
const opts = { useNewUrlParser: true };

mongoose.set('useFindAndModify', false);
mongoose.set('useCreateIndex', true);
mongoose.set('debug', true);

const dashboardSchema = new Schema({
  userId: Schema.Types.ObjectId
},
{
  toJSON: { virtuals: true },
  toObject: { virtuals: true }
});

dashboardSchema.virtual('TopReports').get(function() {
  return Report.find({ userId: this.userId }).sort("-date").limit(10);
});

const reportSchema = new Schema({
  userId: Schema.Types.ObjectId,
  seq: Number,
  date: Date
});

const Dashboard = mongoose.model('Dashboard', dashboardSchema);
const Report = mongoose.model('Report', reportSchema);

const log = data => console.log(JSON.stringify(data, undefined, 2));

(async function() {

  try {

    const conn = await mongoose.connect(uri, opts);

    await Promise.all(
      Object.entries(conn.models).map(([k,m]) => m.deleteMany())
    );

    // Insert some things
    let { userId } = await Dashboard.create({ userId: new ObjectId() });

    const oneDay = ( 1000 * 60 * 60 * 24 );
    const baseDate = Date.now() - (oneDay * 30); // 30 days ago

    await Report.insertMany(
      [ ...Array(20)]
        .map((e,i) => ({ userId, seq: i+1, date: baseDate + ( oneDay * i ) }))
    );

    // Mimic the virtual populate with the getter
    let results = await Dashboard.find();
    for ( let r of results ) {
      let obj = { ...r.toObject() };        // copy the plain object data only
      obj.TopReports = await r.TopReports;  // Resolve the Promise
      log(obj);
    }

  } catch (e) {
    console.error(e)
  } finally {
    mongoose.disconnect()
  }

})()

And output:

Mongoose: dashboards.deleteMany({}, {})
Mongoose: reports.deleteMany({}, {})
Mongoose: dashboards.insertOne({ _id: ObjectId("5cce45193134aa37e88c4114"), userId: ObjectId("5cce45193134aa37e88c4113"), __v: 0 })
Mongoose: reports.insertMany([ { _id: 5cce45193134aa37e88c4115, userId: 5cce45193134aa37e88c4113, seq: 1, date: 2019-04-05T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4116, userId: 5cce45193134aa37e88c4113, seq: 2, date: 2019-04-06T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4117, userId: 5cce45193134aa37e88c4113, seq: 3, date: 2019-04-07T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4118, userId: 5cce45193134aa37e88c4113, seq: 4, date: 2019-04-08T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4119, userId: 5cce45193134aa37e88c4113, seq: 5, date: 2019-04-09T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c411a, userId: 5cce45193134aa37e88c4113, seq: 6, date: 2019-04-10T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c411b, userId: 5cce45193134aa37e88c4113, seq: 7, date: 2019-04-11T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c411c, userId: 5cce45193134aa37e88c4113, seq: 8, date: 2019-04-12T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c411d, userId: 5cce45193134aa37e88c4113, seq: 9, date: 2019-04-13T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c411e, userId: 5cce45193134aa37e88c4113, seq: 10, date: 2019-04-14T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c411f, userId: 5cce45193134aa37e88c4113, seq: 11, date: 2019-04-15T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4120, userId: 5cce45193134aa37e88c4113, seq: 12, date: 2019-04-16T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4121, userId: 5cce45193134aa37e88c4113, seq: 13, date: 2019-04-17T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4122, userId: 5cce45193134aa37e88c4113, seq: 14, date: 2019-04-18T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4123, userId: 5cce45193134aa37e88c4113, seq: 15, date: 2019-04-19T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4124, userId: 5cce45193134aa37e88c4113, seq: 16, date: 2019-04-20T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4125, userId: 5cce45193134aa37e88c4113, seq: 17, date: 2019-04-21T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4126, userId: 5cce45193134aa37e88c4113, seq: 18, date: 2019-04-22T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4127, userId: 5cce45193134aa37e88c4113, seq: 19, date: 2019-04-23T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4128, userId: 5cce45193134aa37e88c4113, seq: 20, date: 2019-04-24T02:06:17.518Z, __v: 0 } ], {})
Mongoose: dashboards.find({}, { projection: {} })
Mongoose: reports.find({ userId: ObjectId("5cce45193134aa37e88c4113") }, { sort: { date: -1 }, limit: 10, projection: {} })
{
  "_id": "5cce45193134aa37e88c4114",
  "userId": "5cce45193134aa37e88c4113",
  "__v": 0,
  "TopReports": [
    {
      "_id": "5cce45193134aa37e88c4128",
      "userId": "5cce45193134aa37e88c4113",
      "seq": 20,
      "date": "2019-04-24T02:06:17.518Z",
      "__v": 0
    },
    {
      "_id": "5cce45193134aa37e88c4127",
      "userId": "5cce45193134aa37e88c4113",
      "seq": 19,
      "date": "2019-04-23T02:06:17.518Z",
      "__v": 0
    },
    {
      "_id": "5cce45193134aa37e88c4126",
      "userId": "5cce45193134aa37e88c4113",
      "seq": 18,
      "date": "2019-04-22T02:06:17.518Z",
      "__v": 0
    },
    {
      "_id": "5cce45193134aa37e88c4125",
      "userId": "5cce45193134aa37e88c4113",
      "seq": 17,
      "date": "2019-04-21T02:06:17.518Z",
      "__v": 0
    },
    {
      "_id": "5cce45193134aa37e88c4124",
      "userId": "5cce45193134aa37e88c4113",
      "seq": 16,
      "date": "2019-04-20T02:06:17.518Z",
      "__v": 0
    },
    {
      "_id": "5cce45193134aa37e88c4123",
      "userId": "5cce45193134aa37e88c4113",
      "seq": 15,
      "date": "2019-04-19T02:06:17.518Z",
      "__v": 0
    },
    {
      "_id": "5cce45193134aa37e88c4122",
      "userId": "5cce45193134aa37e88c4113",
      "seq": 14,
      "date": "2019-04-18T02:06:17.518Z",
      "__v": 0
    },
    {
      "_id": "5cce45193134aa37e88c4121",
      "userId": "5cce45193134aa37e88c4113",
      "seq": 13,
      "date": "2019-04-17T02:06:17.518Z",
      "__v": 0
    },
    {
      "_id": "5cce45193134aa37e88c4120",
      "userId": "5cce45193134aa37e88c4113",
      "seq": 12,
      "date": "2019-04-16T02:06:17.518Z",
      "__v": 0
    },
    {
      "_id": "5cce45193134aa37e88c411f",
      "userId": "5cce45193134aa37e88c4113",
      "seq": 11,
      "date": "2019-04-15T02:06:17.518Z",
      "__v": 0
    }
  ],
  "id": "5cce45193134aa37e88c4114"
}

Of course the Promise returned by the getter needs to be resolved for each returned document. By contrast the populate() uses $in in order to return the matching entries for ALL results in the find() with a single request, and this would issue a new find() for every Dashboard document instead of a single find() based on ALL the userId values found in every Dashboard document in the result.

Basically as opposed to either populate() or $lookup you are essentially splitting out the "join" logic into parts of the control flow, where it really does not belong, and becomes difficult to manage as well as generating even more requests back to the server.

这篇关于猫鼬用吸气剂功能填充虚拟字段不起作用的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆