[解決済み] Mongoose で populate の後にクエリを実行する
質問
私は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()
}
})()
関連
-
[解決済み】E11000重複キーエラー mongodb mongooseのインデックス
-
[解決済み] Mongooseで文書を更新/アップサートするにはどうしたらいいですか?
-
[解決済み] Mongooseの"__v "フィールドとは?
-
[解決済み】Mongoose: findOneAndUpdateが更新されたドキュメントを返さない
-
[解決済み] mongodb/mongoose findMany - 配列にリストされたIDを持つすべてのドキュメントを検索する。
-
[解決済み】Node.jsでMongooseを使用してページングする方法は?
-
[解決済み】mongooseの_idと文字列の比較
-
[解決済み] MongoDB/Mongooseは特定の日付にクエリを実行する?
-
[解決済み] mongoose でネストした配列に値を入れる
-
[解決済み] 保存後のMongooseのポップアップ
最新
-
nginxです。[emerg] 0.0.0.0:80 への bind() に失敗しました (98: アドレスは既に使用中です)
-
htmlページでギリシャ文字を使うには
-
ピュアhtml+cssでの要素読み込み効果
-
純粋なhtml + cssで五輪を実現するサンプルコード
-
ナビゲーションバー・ドロップダウンメニューのHTML+CSSサンプルコード
-
タイピング効果を実現するピュアhtml+css
-
htmlの選択ボックスのプレースホルダー作成に関する質問
-
html css3 伸縮しない 画像表示効果
-
トップナビゲーションバーメニュー作成用HTML+CSS
-
html+css 実装 サイバーパンク風ボタン
おすすめ
-
[解決済み] 再インストールを繰り返しても、npm run でモジュール 'sass' が見つからない。
-
[解決済み】Google spreadsheet api Requestに不十分な認証スコープがあった。
-
[解決済み】モジュール 'internal/util/types' が見つかりません。
-
[解決済み] 非推奨パッケージに関するNPM警告メッセージ
-
[解決済み】"npm update -g" の後に "Cannot find module 'npmlog'" というエラーが発生する。
-
[解決済み】ブロックスコープの宣言は、ストリクトモード以外ではまだサポートされていません。
-
[解決済み】nodemon - app crashed - waiting for file changes before start
-
[解決済み] create-react-app、インストールエラー("コマンドが見つからない")。
-
[解決済み] ReferenceError: describe は定義されていません NodeJs
-
[解決済み] Macでポート3000をロックしているプロセスを見つける(そして殺す)【終了