node.js - 在Mongoose中填充后查询

标签 node.js mongodb mongoose

我对Mongoose和MongoDB还不太熟悉,所以我很难搞清楚这样的事情是否有可能:

Item = new Schema({
    id: Schema.ObjectId,
    dateCreated: { type: Date, default: Date.now },
    title: { type: String, default: 'No Title' },
    description: { type: String, default: 'No Description' },
    tags: [ { type: Schema.ObjectId, ref: 'ItemTag' }]
});

ItemTag = new Schema({
    id: Schema.ObjectId,
    tagId: { type: Schema.ObjectId, ref: 'Tag' },
    tagName: { type: String }
});



var query = Models.Item.find({});

query
    .desc('dateCreated')
    .populate('tags')
    .where('tags.tagName').in(['funny', 'politics'])
    .run(function(err, docs){
       // docs is always empty
    });

有更好的办法吗?
编辑
为任何困惑道歉。我要做的是获取所有包含有趣标签或政治标签的项目。
编辑
无where条款的文件:
[{ 
    _id: 4fe90264e5caa33f04000012,
    dislikes: 0,
    likes: 0,
    source: '/uploads/loldog.jpg',
    comments: [],
    tags: [{
        itemId: 4fe90264e5caa33f04000012,
        tagName: 'movies',
        tagId: 4fe64219007e20e644000007,
        _id: 4fe90270e5caa33f04000015,
        dateCreated: Tue, 26 Jun 2012 00:29:36 GMT,
        rating: 0,
        dislikes: 0,
        likes: 0 
    },
    { 
        itemId: 4fe90264e5caa33f04000012,
        tagName: 'funny',
        tagId: 4fe64219007e20e644000002,
        _id: 4fe90270e5caa33f04000017,
        dateCreated: Tue, 26 Jun 2012 00:29:36 GMT,
        rating: 0,
        dislikes: 0,
        likes: 0 
    }],
    viewCount: 0,
    rating: 0,
    type: 'image',
    description: null,
    title: 'dogggg',
    dateCreated: Tue, 26 Jun 2012 00:29:24 GMT 
 }, ... ]

使用where子句,我得到一个空数组。

最佳答案

如果现代MongoDB大于3.2,在大多数情况下可以使用$lookup作为.populate()的替代。这还有一个优点,即实际上是在“服务器上”执行连接,而不是.populate()执行的实际上是“多个查询”来“模拟”连接。
因此,就关系数据库的工作方式而言,.populate()并不是真正的“连接”。另一方面,$lookup操作符实际上在服务器上执行工作,或多或少类似于“left join”:

Item.aggregate(
  [
    { "$lookup": {
      "from": ItemTags.collection.name,
      "localField": "tags",
      "foreignField": "_id",
      "as": "tags"
    }},
    { "$unwind": "$tags" },
    { "$match": { "tags.tagName": { "$in": [ "funny", "politics" ] } } },
    { "$group": {
      "_id": "$_id",
      "dateCreated": { "$first": "$dateCreated" },
      "title": { "$first": "$title" },
      "description": { "$first": "$description" },
      "tags": { "$push": "$tags" }
    }}
  ],
  function(err, result) {
    // "tags" is now filtered by condition and "joined"
  }
)

注意,这里的.collection.name实际上计算为“string”,它是分配给模型的mongodb集合的实际名称。由于Mongoose默认情况下会将集合名称“复数”,并且$lookup需要实际的MongoDB集合名称作为参数(因为这是一个服务器操作),因此这是在Mongoose代码中使用的一个方便的技巧,而不是直接“硬编码”集合名称。
虽然我们也可以在数组上使用$filter来移除不需要的项,但这实际上是最有效的形式,因为对于asAggregation Pipeline Optimization的特殊条件,后面是a$lookup和a$unwind条件。
这实际上导致三个管道阶段合二为一:
   { "$lookup" : {
     "from" : "itemtags",
     "as" : "tags",
     "localField" : "tags",
     "foreignField" : "_id",
     "unwinding" : {
       "preserveNullAndEmptyArrays" : false
     },
     "matching" : {
       "tagName" : {
         "$in" : [
           "funny",
           "politics"
         ]
       }
     }
   }}

这是高度优化的,因为实际操作“首先筛选要联接的集合”,然后返回结果并“展开”数组。这两种方法都被使用,因此结果不会打破bson限制16mb,这是客户端没有的限制。
唯一的问题是,它在某些方面似乎是“反直觉的”,特别是当您想要数组中的结果时,但这就是$match在这里的作用,因为它重新构造为原始文档形式。
不幸的是,我们现在还不能用服务器使用的最终语法编写$group。嗯,这是一个需要纠正的疏忽。但是现在,简单地使用序列就可以了,而且是最可行的选择,具有最好的性能和可伸缩性。
附录-MongoDB 3.6及以上版本
尽管这里显示的模式由于其他阶段是如何进入$lookup而相当优化,但它确实有一个缺点,即通常$lookup$lookup的固有的“左连接”被这里的populate()的“最佳”使用否定,而$unwind不保留空数组。你可以添加preserveNullAndEmptyArrays选项,但这否定了上面描述的“优化”序列,基本上保留了所有三个阶段的完整性,而这三个阶段通常会在优化中组合在一起。
MongoDB 3.6扩展为“更具表现力”的$lookup形式,允许使用“子管道”表达式。它不仅满足了保留“left join”的目标,而且还允许使用优化查询来减少返回的结果,并且语法简单得多:
Item.aggregate([
  { "$lookup": {
    "from": ItemTags.collection.name,
    "let": { "tags": "$tags" },
    "pipeline": [
      { "$match": {
        "tags": { "$in": [ "politics", "funny" ] },
        "$expr": { "$in": [ "$_id", "$$tags" ] }
      }}
    ]
  }}
])

为了将声明的“local”值与“foreign”值匹配而使用的$expr实际上是MongoDB现在使用原始$lookup语法“internal”所做的。通过以这种形式表达,我们可以在“子管道”中定制初始$match表达式。
事实上,作为一个真正的“聚合管道”,您可以使用此“子管道”表达式中的聚合管道执行任何操作,包括将$lookup的级别“嵌套”到其他相关集合。
进一步的使用有点超出了问题的范围,但是对于甚至“嵌套人口”来说,$lookup的新使用模式允许这一点几乎相同,并且在它的完全使用中“更强大”。
工作实例
下面给出一个在模型上使用静态方法的示例。一旦实现了静态方法,调用就变成:
  Item.lookup(
    {
      path: 'tags',
      query: { 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } }
    },
    callback
  )

或者增强到更现代一点甚至变成:
  let results = await Item.lookup({
    path: 'tags',
    query: { 'tagName' : { '$in': [ 'funny', 'politics' ] } }
  })

使其在结构上非常类似于.populate(),但实际上它是在服务器上进行连接。为了完整起见,这里的用法根据父实例和子实例将返回的数据返回到的Mongoose文档实例。
这是相当琐碎和容易适应或只是使用,因为是最常见的情况。
注意:这里使用async只是为了简单地运行所附示例。实际的实现没有这种依赖关系。
const async = require('async'),
      mongoose = require('mongoose'),
      Schema = mongoose.Schema;

mongoose.Promise = global.Promise;
mongoose.set('debug', true);
mongoose.connect('mongodb://localhost/looktest');

const itemTagSchema = new Schema({
  tagName: String
});

const itemSchema = new Schema({
  dateCreated: { type: Date, default: Date.now },
  title: String,
  description: String,
  tags: [{ type: Schema.Types.ObjectId, ref: 'ItemTag' }]
});

itemSchema.statics.lookup = function(opt,callback) {
  let rel =
    mongoose.model(this.schema.path(opt.path).caster.options.ref);

  let group = { "$group": { } };
  this.schema.eachPath(p =>
    group.$group[p] = (p === "_id") ? "$_id" :
      (p === opt.path) ? { "$push": `$${p}` } : { "$first": `$${p}` });

  let pipeline = [
    { "$lookup": {
      "from": rel.collection.name,
      "as": opt.path,
      "localField": opt.path,
      "foreignField": "_id"
    }},
    { "$unwind": `$${opt.path}` },
    { "$match": opt.query },
    group
  ];

  this.aggregate(pipeline,(err,result) => {
    if (err) callback(err);
    result = result.map(m => {
      m[opt.path] = m[opt.path].map(r => rel(r));
      return this(m);
    });
    callback(err,result);
  });
}

const Item = mongoose.model('Item', itemSchema);
const ItemTag = mongoose.model('ItemTag', itemTagSchema);

function log(body) {
  console.log(JSON.stringify(body, undefined, 2))
}
async.series(
  [
    // Clean data
    (callback) => async.each(mongoose.models,(model,callback) =>
      model.remove({},callback),callback),

    // Create tags and items
    (callback) =>
      async.waterfall(
        [
          (callback) =>
            ItemTag.create([{ "tagName": "movies" }, { "tagName": "funny" }],
              callback),

          (tags, callback) =>
            Item.create({ "title": "Something","description": "An item",
              "tags": tags },callback)
        ],
        callback
      ),

    // Query with our static
    (callback) =>
      Item.lookup(
        {
          path: 'tags',
          query: { 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } }
        },
        callback
      )
  ],
  (err,results) => {
    if (err) throw err;
    let result = results.pop();
    log(result);
    mongoose.disconnect();
  }
)

或者对于node 8.x及更高版本,使用async/await且没有附加依赖项时更现代一点:
const { Schema } = mongoose = require('mongoose');
const uri = 'mongodb://localhost/looktest';

mongoose.Promise = global.Promise;
mongoose.set('debug', true);

const itemTagSchema = new Schema({
  tagName: String
});

const itemSchema = new Schema({
  dateCreated: { type: Date, default: Date.now },
  title: String,
  description: String,
  tags: [{ type: Schema.Types.ObjectId, ref: 'ItemTag' }]
});

itemSchema.statics.lookup = function(opt) {
  let rel =
    mongoose.model(this.schema.path(opt.path).caster.options.ref);

  let group = { "$group": { } };
  this.schema.eachPath(p =>
    group.$group[p] = (p === "_id") ? "$_id" :
      (p === opt.path) ? { "$push": `$${p}` } : { "$first": `$${p}` });

  let pipeline = [
    { "$lookup": {
      "from": rel.collection.name,
      "as": opt.path,
      "localField": opt.path,
      "foreignField": "_id"
    }},
    { "$unwind": `$${opt.path}` },
    { "$match": opt.query },
    group
  ];

  return this.aggregate(pipeline).exec().then(r => r.map(m => 
    this({ ...m, [opt.path]: m[opt.path].map(r => rel(r)) })
  ));
}

const Item = mongoose.model('Item', itemSchema);
const ItemTag = mongoose.model('ItemTag', itemTagSchema);

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

(async function() {
  try {

    const conn = await mongoose.connect(uri);

    // Clean data
    await Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()));

    // Create tags and items
    const tags = await ItemTag.create(
      ["movies", "funny"].map(tagName =>({ tagName }))
    );
    const item = await Item.create({ 
      "title": "Something",
      "description": "An item",
      tags 
    });

    // Query with our static
    const result = (await Item.lookup({
      path: 'tags',
      query: { 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } }
    })).pop();
    log(result);

    mongoose.disconnect();

  } catch (e) {
    console.error(e);
  } finally {
    process.exit()
  }
})()

从MongoDB 3.6及以上,即使没有$unwind$group建筑:
const { Schema, Types: { ObjectId } } = mongoose = require('mongoose');

const uri = 'mongodb://localhost/looktest';

mongoose.Promise = global.Promise;
mongoose.set('debug', true);

const itemTagSchema = new Schema({
  tagName: String
});

const itemSchema = new Schema({
  title: String,
  description: String,
  tags: [{ type: Schema.Types.ObjectId, ref: 'ItemTag' }]
},{ timestamps: true });

itemSchema.statics.lookup = function({ path, query }) {
  let rel =
    mongoose.model(this.schema.path(path).caster.options.ref);

  // MongoDB 3.6 and up $lookup with sub-pipeline
  let pipeline = [
    { "$lookup": {
      "from": rel.collection.name,
      "as": path,
      "let": { [path]: `$${path}` },
      "pipeline": [
        { "$match": {
          ...query,
          "$expr": { "$in": [ "$_id", `$$${path}` ] }
        }}
      ]
    }}
  ];

  return this.aggregate(pipeline).exec().then(r => r.map(m =>
    this({ ...m, [path]: m[path].map(r => rel(r)) })
  ));
};

const Item = mongoose.model('Item', itemSchema);
const ItemTag = mongoose.model('ItemTag', itemTagSchema);

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

(async function() {

  try {

    const conn = await mongoose.connect(uri);

    // Clean data
    await Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()));

    // Create tags and items
    const tags = await ItemTag.insertMany(
      ["movies", "funny"].map(tagName => ({ tagName }))
    );

    const item = await Item.create({
      "title": "Something",
      "description": "An item",
      tags
    });

    // Query with our static
    let result = (await Item.lookup({
      path: 'tags',
      query: { 'tagName': { '$in': [ 'funny', 'politics' ] } }
    })).pop();
    log(result);


    await mongoose.disconnect();

  } catch(e) {
    console.error(e)
  } finally {
    process.exit()
  }

})()

关于node.js - 在Mongoose中填充后查询,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48609422/

相关文章:

node.js - expressjs发帖时出现403 forbidden错误

javascript - GitHub 速率限制防止返回用户提交历史记录

mongodb - Mongo : Find documents that have 0 associated documents, 更便宜

node.js - Mongoose / express : average of subdocuments

node.js - NodeJS Socket.io 私有(private)消息代码不起作用

javascript - Mongoose 模式无法读取未定义的属性 'password'

node.js - Mongoose 模式包含整个数据库对象而不是定义的模式对象

node.js - Sequelize 插入行多对多关系

node.js - 如何使用私有(private)IP在两个项目之间共享Google Cloud SQL实例?

Android:无法从多维 MongoDB jsonObject 转换为 JSONArray