Saat menggunakan $in
klausa MongoDB , apakah urutan dokumen yang dikembalikan selalu sesuai dengan urutan argumen array?
Saat menggunakan $in
klausa MongoDB , apakah urutan dokumen yang dikembalikan selalu sesuai dengan urutan argumen array?
Jawaban:
Sebagaimana dicatat, urutan argumen dalam larik klausa $ in tidak mencerminkan urutan bagaimana dokumen diambil. Itu tentu saja akan menjadi urutan alami atau dengan urutan indeks yang dipilih seperti yang ditunjukkan.
Jika Anda perlu mempertahankan tatanan ini, pada dasarnya Anda memiliki dua opsi.
Jadi katakanlah Anda mencocokkan nilai _id
dalam dokumen Anda dengan array yang akan diteruskan ke $in
as [ 4, 2, 8 ]
.
var list = [ 4, 2, 8 ];
db.collection.aggregate([
// Match the selected documents by "_id"
{ "$match": {
"_id": { "$in": [ 4, 2, 8 ] },
},
// Project a "weight" to each document
{ "$project": {
"weight": { "$cond": [
{ "$eq": [ "$_id", 4 ] },
1,
{ "$cond": [
{ "$eq": [ "$_id", 2 ] },
2,
3
]}
]}
}},
// Sort the results
{ "$sort": { "weight": 1 } }
])
Jadi itu akan menjadi bentuk yang diperluas. Apa yang pada dasarnya terjadi di sini adalah bahwa sama seperti larik nilai yang diteruskan kepada $in
Anda, Anda juga membuat pernyataan "bersarang" $cond
untuk menguji nilai dan menetapkan bobot yang sesuai. Karena nilai "bobot" tersebut mencerminkan urutan elemen dalam larik, Anda kemudian dapat meneruskan nilai tersebut ke tahap pengurutan untuk mendapatkan hasil dalam urutan yang diperlukan.
Tentu saja Anda benar-benar "membangun" pernyataan pipeline dalam kode, seperti ini:
var list = [ 4, 2, 8 ];
var stack = [];
for (var i = list.length - 1; i > 0; i--) {
var rec = {
"$cond": [
{ "$eq": [ "$_id", list[i-1] ] },
i
]
};
if ( stack.length == 0 ) {
rec["$cond"].push( i+1 );
} else {
var lval = stack.pop();
rec["$cond"].push( lval );
}
stack.push( rec );
}
var pipeline = [
{ "$match": { "_id": { "$in": list } }},
{ "$project": { "weight": stack[0] }},
{ "$sort": { "weight": 1 } }
];
db.collection.aggregate( pipeline );
Tentu saja jika semua itu tampak lumayan untuk kepekaan Anda, maka Anda dapat melakukan hal yang sama menggunakan mapReduce, yang terlihat lebih sederhana tetapi kemungkinan akan berjalan agak lebih lambat.
var list = [ 4, 2, 8 ];
db.collection.mapReduce(
function () {
var order = inputs.indexOf(this._id);
emit( order, { doc: this } );
},
function() {},
{
"out": { "inline": 1 },
"query": { "_id": { "$in": list } },
"scope": { "inputs": list } ,
"finalize": function (key, value) {
return value.doc;
}
}
)
Dan itu pada dasarnya bergantung pada nilai "kunci" yang dipancarkan berada dalam "urutan indeks" bagaimana mereka muncul dalam larik masukan.
Jadi pada dasarnya itu adalah cara Anda mempertahankan urutan daftar input ke $in
kondisi di mana Anda sudah memiliki daftar itu dalam urutan yang ditentukan.
Cara lain menggunakan kueri Agregasi hanya berlaku untuk MongoDB verion> = 3.4 -
Penghargaan untuk kebaikan ini entri blog yang .
Contoh dokumen yang akan diambil dalam pesanan ini -
var order = [ "David", "Charlie", "Tess" ];
Kueri -
var query = [
{$match: {name: {$in: order}}},
{$addFields: {"__order": {$indexOfArray: [order, "$name" ]}}},
{$sort: {"__order": 1}}
];
var result = db.users.aggregate(query);
Kutipan lain dari posting yang menjelaskan operator agregasi ini digunakan -
Tahap "$ addFields" baru di 3.4 dan ini memungkinkan Anda untuk "$ project" bidang baru ke dokumen yang ada tanpa mengetahui semua bidang lain yang ada. Ekspresi "$ indexOfArray" baru mengembalikan posisi elemen tertentu dalam larik tertentu.
Pada dasarnya addFields
operator menambahkan order
bidang baru ke setiap dokumen ketika ditemukan dan order
bidang ini mewakili urutan asli dari larik yang kami sediakan. Kemudian kami cukup mengurutkan dokumen berdasarkan bidang ini.
Jika Anda tidak ingin menggunakan aggregate
, solusi lain adalah menggunakan find
dan kemudian mengurutkan hasil dokumen sisi klien menggunakan array#sort
:
Jika $in
nilainya adalah tipe primitif seperti angka, Anda dapat menggunakan pendekatan seperti:
var ids = [4, 2, 8, 1, 9, 3, 5, 6];
MyModel.find({ _id: { $in: ids } }).exec(function(err, docs) {
docs.sort(function(a, b) {
// Sort docs by the order of their _id values in ids.
return ids.indexOf(a._id) - ids.indexOf(b._id);
});
});
Jika $in
nilainya adalah tipe non-primitif seperti ObjectId
s, pendekatan lain diperlukan sebagai indexOf
perbandingan dengan referensi dalam kasus tersebut.
Jika Anda menggunakan Node.js 4.x +, Anda dapat menggunakan Array#findIndex
dan ObjectID#equals
menangani ini dengan mengubah sort
fungsinya menjadi:
docs.sort((a, b) => ids.findIndex(id => a._id.equals(id)) -
ids.findIndex(id => b._id.equals(id)));
Atau dengan versi Node.js apa pun, dengan garis bawah / lodash findIndex
:
docs.sort(function (a, b) {
return _.findIndex(ids, function (id) { return a._id.equals(id); }) -
_.findIndex(ids, function (id) { return b._id.equals(id); });
});
Document#equals
untuk membandingkan dengan bidang dokumen _id
. Diperbarui untuk membuat _id
perbandingan menjadi eksplisit. Terima kasih untuk bertanya.
Mirip dengan solusi JonnyHK , Anda dapat menyusun ulang dokumen yang dikembalikan dari find
klien Anda (jika klien Anda menggunakan JavaScript) dengan kombinasi map
dan Array.prototype.find
fungsi di EcmaScript 2015:
Collection.find({ _id: { $in: idArray } }).toArray(function(err, res) {
var orderedResults = idArray.map(function(id) {
return res.find(function(document) {
return document._id.equals(id);
});
});
});
Beberapa catatan:
idArray
adalah larikObjectId
map
callback untuk menyederhanakan kode Anda.find
melintasi larik untuk setiap elemen larik (dari luar map
). Ini sangat tidak efisien, karena ada solusi O (n) menggunakan tabel pencarian.
Saya tahu pertanyaan ini terkait dengan kerangka JS Mongoose, tetapi yang digandakan bersifat umum, jadi saya harap memposting solusi Python (PyMongo) baik-baik saja di sini.
things = list(db.things.find({'_id': {'$in': id_array}}))
things.sort(key=lambda thing: id_array.index(thing['_id']))
# things are now sorted according to id_array order
Cara mudah untuk mengurutkan hasil setelah mongo mengembalikan larik adalah dengan membuat objek dengan id sebagai kunci dan kemudian memetakan di atas _id yang diberikan untuk mengembalikan larik yang diurutkan dengan benar.
async function batchUsers(Users, keys) {
const unorderedUsers = await Users.find({_id: {$in: keys}}).toArray()
let obj = {}
unorderedUsers.forEach(x => obj[x._id]=x)
const ordered = keys.map(key => obj[key])
return ordered
}
Saya tahu ini adalah utas lama, tetapi jika Anda baru saja mengembalikan nilai Id dalam larik, Anda mungkin harus memilih sintaks ini. Karena saya tidak bisa mendapatkan nilai indexOf untuk mencocokkan dengan format ObjectId mongo.
obj.map = function() {
for(var i = 0; i < inputs.length; i++){
if(this._id.equals(inputs[i])) {
var order = i;
}
}
emit(order, {doc: this});
};
Anda dapat menjamin pesanan dengan $ atau klausul.
Jadi gunakan $or: [ _ids.map(_id => ({_id}))]
saja.
$or
solusi tidak bekerja sejak v2.6 .
Ini adalah solusi kode setelah hasil diambil dari Mongo. Menggunakan peta untuk menyimpan indeks dan kemudian menukar nilai.
catDetails := make([]CategoryDetail, 0)
err = sess.DB(mdb).C("category").
Find(bson.M{
"_id": bson.M{"$in": path},
"is_active": 1,
"name": bson.M{"$ne": ""},
"url.path": bson.M{"$exists": true, "$ne": ""},
}).
Select(
bson.M{
"is_active": 1,
"name": 1,
"url.path": 1,
}).All(&catDetails)
if err != nil{
return
}
categoryOrderMap := make(map[int]int)
for index, v := range catDetails {
categoryOrderMap[v.Id] = index
}
counter := 0
for i := 0; counter < len(categoryOrderMap); i++ {
if catId := int(path[i].(float64)); catId > 0 {
fmt.Println("cat", catId)
if swapIndex, exists := categoryOrderMap[catId]; exists {
if counter != swapIndex {
catDetails[swapIndex], catDetails[counter] = catDetails[counter], catDetails[swapIndex]
categoryOrderMap[catId] = counter
categoryOrderMap[catDetails[swapIndex].Id] = swapIndex
}
counter++
}
}
}