在猫鼬中填充后查询


81

一般来说,我对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子句,我得到一个空数组。

Answers:


59

使用大于3.2的现代MongoDB,您可以在大多数情况下$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这里实际上计算的是“字符串”,它是分配给模型的MongoDB集合的实际名称。由于猫鼬默认情况下会“复数化”集合名称,并且$lookup需要实际的MongoDB集合名称作为参数(因为这是服务器操作),因此在猫鼬代码中使用这是一个方便的技巧,而不是直接对集合名称进行“硬编码” 。

尽管我们也可以$filter在数组上使用以删除不需要的项,但实际上这是最有效的形式,这是由于针对条件as和条件an的特殊条件进行了聚合管道优化$lookup$unwind$match

实际上,这导致三个管道阶段被分解为一个阶段:

   { "$lookup" : {
     "from" : "itemtags",
     "as" : "tags",
     "localField" : "tags",
     "foreignField" : "_id",
     "unwinding" : {
       "preserveNullAndEmptyArrays" : false
     },
     "matching" : {
       "tagName" : {
         "$in" : [
           "funny",
           "politics"
         ]
       }
     }
   }}

这是最佳选择,因为实际操作会“首先过滤要加入的集合”,然后返回结果并“展开”数组。两种方法都被采用,因此结果不会超过16MB的BSON限制,这是客户端没有的限制。

唯一的问题是,在某些方面它似乎“违反直觉”,尤其是当您希望将结果存储在数组中时,但这$group就是这里的意义,因为它可以重构为原始文档格式。

不幸的是,我们此时根本无法$lookup使用服务器使用的最终语法进行编写。恕我直言,这是一个需要纠正的疏忽。但是就目前而言,简单地使用序列即可,并且是具有最佳性能和可伸缩性的最可行选择。

附录-MongoDB 3.6及更高版本

尽管此处显示的模式由于其他阶段如何进入而已进行了相当优化$lookup,但它确实存在一个失败之处,即这通常是两者固有的“ LEFT JOIN”,$lookuppopulate()通过的“最佳”使用则否定了$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" ] }
      }}
    ]
  }}
])

$expr以匹配使用的已声明与“洋”价值“本地”值实际上是MongoDB中做什么“内部”现在与原来的$lookup语法。通过以这种形式表达,我们可以$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()结构非常相似,但实际上是在服务器上进行联接。为了完整起见,此处的用法将根据父案例和子案例将返回的数据强制转换回猫鼬文档实例。

在大多数情况下,它相当琐碎且易于调整或使用。

注意:此处使用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()
  }

})()

3
我不再使用Mongo / Mongoose,但是我已经接受了您的回答,因为这是一个很普遍的问题,而且看起来对其他人有所帮助。很高兴看到这个问题现在有了更可扩展的解决方案。感谢您提供更新的答案。
jschr

40

您所要求的不是直接支持的,而是可以通过在查询返回后添加另一个过滤器步骤来实现。

首先,.populate( 'tags', null, { tagName: { $in: ['funny', 'politics'] } } )绝对是过滤标签文档所需要做的。然后,查询返回后,您将需要手动过滤掉没有任何tags符合填充条件的文档的文档。就像是:

query....
.exec(function(err, docs){
   docs = docs.filter(function(doc){
     return doc.tags.length;
   })
   // do stuff with docs
});

1
嘿亚伦,谢谢你的答复。我可能是错的,但是populate()上的$ in不会只填充匹配的标签吗?因此,该项目上的所有其他标签都将被过滤掉。听起来我必须填充所有项目,然后进行第二个过滤步骤,然后根据标签名称将其减少。
jschr 2012年

@aaronheckmann我已经实现了建议的解决方案,您可以在.exec之后进行过滤,因为尽管填充查询仅填充必需的对象,但仍填充其返回的整个数据集。您是否认为在Mongoose的较新版本中,有一些选项可以仅返回填充的数据集,因此我们无需进行其他过滤?
Aqib Mumtaz 2015年

我也很想知道性能,如果查询最后返回了整个数据集,那么是否没有进行种群过滤的目的?你说什么?我正在调整总体查询以优化性能,但是这样对于大型数据集,性能不会更好吗?
Aqib Mumtaz 2015年

mongoosejs.com/docs/api.html#query_Query-populate具有所有其他信息,如果有其他人有兴趣
samazi

填充后在不同字段中如何匹配?
nicogaldo

19

尝试更换

.populate('tags').where('tags.tagName').in(['funny', 'politics']) 

通过

.populate( 'tags', null, { tagName: { $in: ['funny', 'politics'] } } )

1
谢谢回复。我相信这样做只会使每个项目充满滑稽或政治色彩,而不会减少父级列表。我真正想要的只是标签上带有滑稽或政治色彩的物品。
jschr 2012年

您可以显示文档的外观吗?对我来说,在标签数组中遍历“ where”似乎是一个有效的操作。我们是否只是语法错误?您是否尝试过完全删除“ where”子句并检查是否返回了任何内容?另外,只是为了测试编写“ tags.tagName”在语法上是否可行,您可能会暂时忘记ref并尝试使用“ Item”文档中的嵌入式数组进行查询。
Aafreen谢赫2012年

用文档编辑了我的原始帖子。我能够成功地使用模型作为Item内的嵌入式数组对其进行测试,但是不幸的是,由于ItemTag经常更新,因此我要求它是DBRef。再次感谢您的帮助。
jschr 2012年

15

更新:请看一下评论-该答案与问题不正确匹配,但也许它回答了遇到的用户的其他问题(我认为是由于投票),所以我不会删除此“答案”:

首先:我知道这个问题确实过时了,但是我恰好搜索了这个问题,因此该帖子是Google条目#1。因此,我实现了docs.filter版本(可接受的答案),但是当我在猫鼬v4.6.0文档中阅读时,我们现在可以简单地使用:

Item.find({}).populate({
    path: 'tags',
    match: { tagName: { $in: ['funny', 'politics'] }}
}).exec((err, items) => {
  console.log(items.tags) 
  // contains only tags where tagName is 'funny' or 'politics'
})

希望这对将来的搜索机器用户有所帮助。


3
但这只会过滤掉items.tags数组吗?无论tagName是什么,都将返回项目...
OllyBarca

1
没错,@ OllyBarca。根据文档,匹配仅影响人口查询。
andreimarinescu

1
我觉得这不回答这个问题
Z.Alpha

1
@Fabian这不是错误。仅填充查询(在本例中为fans)被过滤。返回的实际文档(作为属性Story包含fans)不会受到影响或过滤。
EnKrypt '18年

2
因此,由于评论中提到的原因,该答案是不正确的。今后任何人都应该注意这一点。
EnKrypt '18年

3

我本人最近遇到相同的问题后,提出了以下解决方案:

首先,找到tagName为“ funny”或“ politics”的所有ItemTag,然后返回ItemTag _ids数组。

然后,找到包含标签数组中所有ItemTag _id的Items

ItemTag
  .find({ tagName : { $in : ['funny','politics'] } })
  .lean()
  .distinct('_id')
  .exec((err, itemTagIds) => {
     if (err) { console.error(err); }
     Item.find({ tag: { $all: itemTagIds} }, (err, items) => {
        console.log(items); // Items filtered by tagName
     });
  });

我是怎么做到的const tagsIds =等待this.tagModel .find({name:{$ in:tags}}).lean().distinct('_ id'); 返回this.adviceModel.find({标签:{$ all:tagsIds}});
Dragos Lupei

1

@aaronheckmann的答案对我有用,但我必须替换return doc.tags.length;为,return doc.tags != null;因为如果该字段与填充在内部的条件不匹配,则该字段包含null。所以最后的代码:

query....
.exec(function(err, docs){
   docs = docs.filter(function(doc){
     return doc.tags != null;
   })
   // do stuff with docs
});
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.