(MongoDB で) 複数のコレクションのデータを 1 つのコレクションに結合するにはどうすればよいですか?
map-reduce を使用できますか?
私は初心者なので、いくつかの例をいただければ幸いです。
(MongoDB で) 複数のコレクションのデータを 1 つのコレクションに結合するにはどうすればよいですか?
map-reduce を使用できますか?
私は初心者なので、いくつかの例をいただければ幸いです。
これをリアルタイムで行うことはできませんが、MongoDB 1.8+ map / reduceの「reduce」outオプションを使用して、map-reduceを複数回実行し、データをマージすることができます(http://www.mongodb.org/を参照)。 display / DOCS / MapReduce#MapReduce-Outputoptions)。両方のコレクションに、_idとして使用できるキーが必要です。
たとえば、users
コレクションとcomments
コレクションがあり、コメントごとにユーザーの人口統計情報を含む新しいコレクションが必要だとします。
users
コレクションに次のフィールドがあるとします。
そして、comments
コレクションには次のフィールドがあります。
このmap/reduceを実行します。
var mapUsers, mapComments, reduce;
db.users_comments.remove();
// setup sample data - wouldn't actually use this in production
db.users.remove();
db.comments.remove();
db.users.save({firstName:"Rich",lastName:"S",gender:"M",country:"CA",age:"18"});
db.users.save({firstName:"Rob",lastName:"M",gender:"M",country:"US",age:"25"});
db.users.save({firstName:"Sarah",lastName:"T",gender:"F",country:"US",age:"13"});
var users = db.users.find();
db.comments.save({userId: users[0]._id, "comment": "Hey, what's up?", created: new ISODate()});
db.comments.save({userId: users[1]._id, "comment": "Not much", created: new ISODate()});
db.comments.save({userId: users[0]._id, "comment": "Cool", created: new ISODate()});
// end sample data setup
mapUsers = function() {
var values = {
country: this.country,
gender: this.gender,
age: this.age
};
emit(this._id, values);
};
mapComments = function() {
var values = {
commentId: this._id,
comment: this.comment,
created: this.created
};
emit(this.userId, values);
};
reduce = function(k, values) {
var result = {}, commentFields = {
"commentId": '',
"comment": '',
"created": ''
};
values.forEach(function(value) {
var field;
if ("comment" in value) {
if (!("comments" in result)) {
result.comments = [];
}
result.comments.push(value);
} else if ("comments" in value) {
if (!("comments" in result)) {
result.comments = [];
}
result.comments.push.apply(result.comments, value.comments);
}
for (field in value) {
if (value.hasOwnProperty(field) && !(field in commentFields)) {
result[field] = value[field];
}
}
});
return result;
};
db.users.mapReduce(mapUsers, reduce, {"out": {"reduce": "users_comments"}});
db.comments.mapReduce(mapComments, reduce, {"out": {"reduce": "users_comments"}});
db.users_comments.find().pretty(); // see the resulting collection
この時点でusers_comments
、マージされたデータを含むという新しいコレクションが作成され、それを使用できるようになります。これらの縮小されたコレクションはすべて_id
、マップ関数で出力したキーであり、すべての値はvalue
キー内のサブオブジェクトです。値は、これらの縮小されたドキュメントの最上位にはありません。
これはやや単純な例です。削減されたコレクションを構築し続けたい限り、より多くのコレクションでこれを繰り返すことができます。また、プロセスでデータの要約と集計を行うこともできます。既存のフィールドを集約および保存するためのロジックがより複雑になるため、複数のreduce関数を定義する可能性があります。
また、ユーザーごとに1つのドキュメントがあり、そのユーザーのすべてのコメントが配列に含まれていることにも注意してください。1対多ではなく1対1の関係にあるデータをマージする場合、データはフラットになり、次のようなreduce関数を使用できます。
reduce = function(k, values) {
var result = {};
values.forEach(function(value) {
var field;
for (field in value) {
if (value.hasOwnProperty(field)) {
result[field] = value[field];
}
}
});
return result;
};
コレクションをフラット化してusers_comments
コメントごとに1つのドキュメントにする場合は、さらに次のコマンドを実行します。
var map, reduce;
map = function() {
var debug = function(value) {
var field;
for (field in value) {
print(field + ": " + value[field]);
}
};
debug(this);
var that = this;
if ("comments" in this.value) {
this.value.comments.forEach(function(value) {
emit(value.commentId, {
userId: that._id,
country: that.value.country,
age: that.value.age,
comment: value.comment,
created: value.created,
});
});
}
};
reduce = function(k, values) {
var result = {};
values.forEach(function(value) {
var field;
for (field in value) {
if (value.hasOwnProperty(field)) {
result[field] = value[field];
}
}
});
return result;
};
db.users_comments.mapReduce(map, reduce, {"out": "comments_with_demographics"});
この手法は、その場で実行するべきではありません。cronジョブなど、マージされたデータを定期的に更新するものに適しています。新しいコレクションで実行ensureIndex
して、それに対して実行するクエリが迅速に実行されるようにすることをお勧めします(データはまだキー内にあることに注意してください。したがって、コメント時間でvalue
インデックスを作成する場合は、次のようになります。comments_with_demographics
created
db.comments_with_demographics.ensureIndex({"value.created": 1});
$lookup を使用した非常に基本的な例。
db.getCollection('users').aggregate([
{
$lookup: {
from: "userinfo",
localField: "userId",
foreignField: "userId",
as: "userInfoData"
}
},
{
$lookup: {
from: "userrole",
localField: "userId",
foreignField: "userId",
as: "userRoleData"
}
},
{ $unwind: { path: "$userInfoData", preserveNullAndEmptyArrays: true }},
{ $unwind: { path: "$userRoleData", preserveNullAndEmptyArrays: true }}
])
ここが使われています
{ $unwind: { path: "$userInfoData", preserveNullAndEmptyArrays: true }},
{ $unwind: { path: "$userRoleData", preserveNullAndEmptyArrays: true }}
それ以外の
{ $unwind:"$userRoleData"}
{ $unwind:"$userRoleData"}
{ $unwind:"$userRoleData"}であるため、$lookup で一致するレコードが見つからない場合、これは空または 0 の結果を返します。
mongodb への一括挿入がない場合は、 内のすべてのオブジェクトをループし、small_collection
1 つずつ に挿入しますbig_collection
。
db.small_collection.find().forEach(function(obj){
db.big_collection.insert(obj)
});
から始めて、新しい集計ステージをの new演算子とMongo 4.4
結合することにより、集計パイプライン内でこの結合を実現できます。$unionWith
$group
$accumulator
// > db.users.find()
// [{ user: 1, name: "x" }, { user: 2, name: "y" }]
// > db.books.find()
// [{ user: 1, book: "a" }, { user: 1, book: "b" }, { user: 2, book: "c" }]
// > db.movies.find()
// [{ user: 1, movie: "g" }, { user: 2, movie: "h" }, { user: 2, movie: "i" }]
db.users.aggregate([
{ $unionWith: "books" },
{ $unionWith: "movies" },
{ $group: {
_id: "$user",
user: {
$accumulator: {
accumulateArgs: ["$name", "$book", "$movie"],
init: function() { return { books: [], movies: [] } },
accumulate: function(user, name, book, movie) {
if (name) user.name = name;
if (book) user.books.push(book);
if (movie) user.movies.push(movie);
return user;
},
merge: function(userV1, userV2) {
if (userV2.name) userV1.name = userV2.name;
userV1.books.concat(userV2.books);
userV1.movies.concat(userV2.movies);
return userV1;
},
lang: "js"
}
}
}}
])
// { _id: 1, user: { books: ["a", "b"], movies: ["g"], name: "x" } }
// { _id: 2, user: { books: ["c"], movies: ["h", "i"], name: "y" } }
$unionWith
すでに集計パイプラインにあるドキュメント内の特定のコレクションからのレコードを結合します。2 つのユニオン ステージの後、パイプライン内にすべてのユーザー、本、および映画のレコードがあります。
次に、オペレーターを使用してアイテムを$group
記録および蓄積し、グループ化されたドキュメントのカスタム蓄積を可能にします。$user
$accumulator
accumulateArgs
ます。init
要素をグループ化するときに蓄積される状態を定義します。accumulate
関数を使用すると、レコードをグループ化してカスタム アクションを実行し、蓄積された状態を構築できます。たとえば、グループ化されているアイテムにbook
フィールドが定義されている場合books
、状態の一部を更新します。merge
2 つの内部状態をマージするために使用されます。これは、シャード クラスターで実行されている集計、または操作がメモリ制限を超えた場合にのみ使用されます。Mongorestore には、データベースに既に存在するものの上に追加する機能があるため、この動作を使用して 2 つのコレクションを組み合わせることができます。
まだ試していませんが、map/reduce アプローチよりも高速に実行される可能性があります。
はい、できます: 今日私が書いた次のユーティリティ関数を使用してください。
function shangMergeCol() {
tcol= db.getCollection(arguments[0]);
for (var i=1; i<arguments.length; i++){
scol= db.getCollection(arguments[i]);
scol.find().forEach(
function (d) {
tcol.insert(d);
}
)
}
}
この関数には任意の数のコレクションを渡すことができ、最初のコレクションがターゲットになります。残りのコレクションはすべて、ターゲット コレクションに転送されるソースです。
アプリケーション層でそれを行う必要があります。ORM を使用している場合は、注釈 (または類似のもの) を使用して、他のコレクションに存在する参照を取得できます。私はMorphiaのみを使用しており、@Reference
注釈はクエリ時に参照エンティティをフェッチするため、コードで自分で行うことを避けることができます。