node.js - 在 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 子句,我得到一个空数组。

最佳答案

使用大于 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"
  }
)

N.B. The .collection.name here actually evaluates to the "string" that is the actual name of the MongoDB collection as assigned to the model. Since mongoose "pluralizes" collection names by default and $lookup needs the actual MongoDB collection name as an argument ( since it's a server operation ), then this is a handy trick to use in mongoose code, as opposed to "hard coding" the collection name directly.



虽然我们也可以在数组上使用 $filter 来删除不需要的项目,但这实际上是最有效的形式,因为 Aggregation Pipeline Optimization 用于特殊条件 $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() 的 Action 被“最佳"此处使用 $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() 非常相似,但它实际上是在服务器上进行连接。为完整起见,此处的用法根据父案例和子案例将返回的数据转换回 mongoose 文档实例。

对于大多数常见情况,它相当简单且易于适应或仅使用。

N.B The use of async here is just for brevity of running the enclosed example. The actual implementation is free of this dependency.


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()
  }

})()

https://stackoverflow.com/questions/11303294/

相关文章:

collections - MongoDB一次查询多个集合

javascript - 如何在 Node.js 中使用带有 Promise 的 MongoDB?

linux - 删除mongodb的日志文件安全吗?

c# - MongoDB GridFs with C#,如何存储图片等文件?

python - flask 用户认证

mongodb - 将 "working set"放入 MongoDB 的 RAM 是什么意思?

javascript - Mongodb如何获取单个文档的大小?

mongodb - MySql 是否有一个示例 MongoDB 数据库?

c# - 在 MongoDB 中将枚举存储为字符串

mongodb - 存储在 MongoDB 中的数组是否保持顺序?