1. ホーム
  2. node.js

[解決済み] Mongoose で populate の後にクエリを実行する

2023-02-14 11:54:32

質問

私は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. .collection.name は実際に評価されるのは "string" で、これはモデルに割り当てられた MongoDB コレクションの実際の名前です。mongoose はデフォルトでコレクション名を複数形にしているので $lookup には実際の MongoDB コレクション名が必要なので (これはサーバーでの操作なので)、 コレクション名を直接指定するのではなく mongoose のコードで使える便利なトリックになっています。

一方 $filter を使用して不要な項目を削除することもできますが、これは実際には 集計パイプラインの最適化 という特殊な条件に対して $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 をサーバーが使用するのと同じ最終的な構文で書くことができないのも残念です。IMHOは、これは修正されるべき見落としであると考えています。しかし、今のところ、単にシーケンスを使用することで動作し、最高のパフォーマンスとスケーラビリティを備えた最も実行可能なオプションです。

補遺 - MongoDB 3.6 以降

ここで示したパターンは かなり最適化されています。 に巻き込まれるため、かなり最適化されています。 $lookup の両方に固有であるLEFT JOIN"を使用すると、1つの失敗があります。 $lookup のアクションと populate() が否定されるのは "最適" の使い方は $unwind を使用すると、空の配列が保存されません。を追加することができます。 preserveNullAndEmptyArrays オプションを追加することができますが、これは "最適化された" のシーケンスは無効になり、通常は最適化で結合されるはずの 3 つのステージがすべてそのままになります。

MongoDB 3.6 の拡張機能として "more expressive" の形で拡張されます。 $lookup という形式で、quot;sub-pipeline" 式を使用することができます。これは、quot;LEFT JOIN"を保持するという目標を満たすだけでなく、返される結果を減らすために最適なクエリを、より単純な構文で実行することを可能にします。

Item.aggregate([
  { "$lookup": {
    "from": ItemTags.collection.name,
    "let": { "tags": "$tags" },
    "pipeline": [
      { "$match": {
        "tags": { "$in": [ "politics", "funny" ] },
        "$expr": { "$in": [ "$_id", "$$tags" ] }
      }}
    ]
  }}
])

この $expr は、宣言された "local" 値と "foreign" 値を一致させるために使用されますが、実は MongoDB が内部的に行っていることで、現在オリジナルの $lookup という構文があります。この形式で表現することで、最初の $match の式を自分たちで調整することができます。

実際には、真の集約パイプラインとして、この "sub-pipeline" 式内の集約パイプラインでできることはほぼすべて行うことができ、quot;nesting" のレベルも含まれます。 $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 の使用は 非同期 の使用は、同封の例を実行するための簡潔さのためだけです。実際の実装はこの依存関係から解放されます。

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

})()