マングースにデータを入力した後のクエリ


83

私は一般的に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句を使用すると、空の配列を取得します。

回答:


61

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

NBザ・.collection.nameここでは、実際にモデルに割り当てられたとしてMongoDBのコレクションの実際の名前である「文字列」と評価されます。mongooseはデフォルトでコレクション名を「複数化」$lookupし、引数として実際のMongoDBコレクション名を必要とするため(サーバー操作であるため)、これはコレクション名を直接「ハードコーディング」するのではなく、mongooseコードで使用する便利なトリックです。 。

$filter配列を使用して不要なアイテムを削除することもできますが、これは実際には、の後にanと条件の両方が続く特別な条件の集約パイプライン最適化により最も効率的な形式です。$lookup$unwind$match

これにより、実際には3つのパイプラインステージが1つにまとめられます。

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

これは、実際の操作が「最初に結合するようにコレクションをフィルタリング」し、次に結果を返し、配列を「巻き戻す」ため、非常に最適です。両方の方法が採用されているため、結果は16MBのBSON制限を超えません。これは、クライアントにはない制約です。

唯一の問題は、特に結果を配列で表示したい場合に、いくつかの点で「直感に反する」ように見えることです$groupが、元のドキュメント形式に再構築されるため、ここではそれが目的です。

また、現時点$lookupでは、サーバーが使用するのと同じ最終的な構文で実際に書き込むことができないのも残念です。私見、これは修正すべき見落としです。しかし今のところ、シーケンスを使用するだけで機能し、最高のパフォーマンスとスケーラビリティを備えた最も実行可能なオプションです。

補遺-MongoDB3.6以降

ここに示されているパターンは、他のステージがどのようにロールインされるかによってかなり最適化され$lookupていますが、通常は両方に固有の「LEFT JOIN」$lookupとのアクションがpopulate()、の「最適な」使用法によって無効になっているという点で失敗しています。$unwindここでは、空の配列は保持されません。preserveNullAndEmptyArraysオプションを追加できますが、これにより、上記の「最適化された」シーケンスが無効になり、通常は最適化で組み合わされる3つのステージすべてがそのまま残ります。

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()構造が非常に似ていますが、実際にはサーバーで結合を行っています。完全を期すために、ここでの使用法は、親と子の両方のケースに従って、返されたデータをマングースドキュメントインスタンスにキャストバックします。

それはかなり些細で、適応するのも簡単で、ほとんどの一般的な場合と同じように使用するのも簡単です。

NBここでの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();
  }
)

または、async/await追加の依存関係がなく、ノード8.x以降の場合はもう少し最新です。

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 2018年

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は一致したタグのみを入力しませんか?そのため、アイテムに追加されたタグはすべて除外されます。すべてのアイテムにデータを入力し、2番目のフィルターステップでタグ名に基づいてアイテムを減らす必要があるようです。
jschr 2012

@aaronheckmann提案されたソリューションを実装しましたが、.execの後にフィルターを実行しようとしています。これは、populateクエリが必要なオブジェクトのみを入力しているにもかかわらず、データセット全体を返すためです。新しいバージョンのMongooseには、入力されたデータセットのみを返すオプションがあるので、別のフィルタリングを行う必要はないと思いますか?
Aqib Mumtaz 2015

また、パフォーマンスについて知りたいと思っています。クエリが最後にデータセット全体を返す場合、母集団フィルタリングを行う目的はありませんか?あなたは何を言っていますか?パフォーマンスの最適化のために人口クエリを適応させていますが、この方法では、大きなデータセットのパフォーマンスは向上しませんか?
Aqib Mumtaz 2015

mongoosejs.com/docs/api.html#query_Query-populateは誰にも興味を持っている場合は、すべての詳細を持っている
samazi

入力時に異なるフィールドでどのように一致しますか?
nicogaldo 2018

20

交換してみてください

.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 Sheikh 2012

ドキュメントを使用して元の投稿を編集しました。Item内の埋め込み配列としてモデルを使用してテストすることはできましたが、ItemTagは頻繁に更新されるため、残念ながらDBRefである必要があります。助けてくれてありがとう。
jschr 2012

15

更新:コメントを見てください-この回答は質問と正しく一致しませんが、出くわしたユーザーの他の質問に回答する可能性があるため(賛成票のためだと思います)、この「回答」は削除しません。

最初に:私はこの質問が本当に時代遅れであることを知っています、しかし私はまさにこの問題を検索しました、そしてこのSO投稿はグーグルエントリー#1でした。そのため、docs.filterバージョン(承認された回答)を実装しましたが、mongoose 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 2016

1
これは質問に答えないと思います
Z.Alpha 2016

1
エラーではない@Fabian。ポピュレーションクエリ(この場合fans)のみがフィルタリングされます。返される実際のドキュメント(つまり、プロパティとしてStory含まfansれている)は、影響を受けたりフィルタリングされたりしません。
EnKrypt 2018年

2
したがって、コメントに記載されている理由により、この回答は正しくありません。将来これを見ている人は誰でも注意する必要があります。
EnKrypt 2018年

3

最近自分で同じ問題を抱えた後、私は次の解決策を思いつきました:

まず、tagNameが「funny」または「politics」のいずれかであるすべてのItemTagを見つけて、ItemTag_idの配列を返します。

次に、tags配列内のすべてのItemTag_idを含むアイテムを検索します

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

どのように私はそれをしましたかconsttagsIds = await this.tagModel .find({name:{$ in:tags}})。lean()。distinct( '_ id'); this.adviceModel.find({タグ:{$ all:tagsIds}});を返します。
DragosLupei19年

1

@aaronheckmannの答えは私にとってはうまくいきましたが、populate内に記述された条件と一致しない場合、そのフィールドにはnullが含まれreturn doc.tags.length;ているreturn doc.tags != null;ため、に置き換える必要がありました。したがって、最終的なコードは次のとおりです。

query....
.exec(function(err, docs){
   docs = docs.filter(function(doc){
     return doc.tags != null;
   })
   // do stuff with docs
});
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.