$ In khoản của MongoDB có đảm bảo thứ tự không


Câu trả lời:


78

Như đã lưu ý, thứ tự của các đối số trong mảng của mệnh đề $ in không phản ánh thứ tự của cách tài liệu được truy xuất. Tất nhiên đó sẽ là thứ tự tự nhiên hoặc theo thứ tự chỉ mục đã chọn như hình.

Nếu bạn cần duy trì đơn đặt hàng này, thì về cơ bản bạn có hai lựa chọn.

Vì vậy, giả sử rằng bạn đang đối sánh các giá trị _idtrong tài liệu của mình với một mảng sẽ được chuyển vào $indưới dạng [ 4, 2, 8 ].

Tiếp cận bằng Aggregate


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

])

Vì vậy, đó sẽ là dạng mở rộng. Điều cơ bản xảy ra ở đây là cũng giống như mảng giá trị được chuyển cho $inbạn cũng tạo một $condcâu lệnh "lồng nhau" để kiểm tra các giá trị và gán trọng số thích hợp. Vì giá trị "trọng số" đó phản ánh thứ tự của các phần tử trong mảng, sau đó bạn có thể chuyển giá trị đó sang một giai đoạn sắp xếp để nhận được kết quả của bạn theo thứ tự bắt buộc.

Tất nhiên bạn thực sự "xây dựng" câu lệnh đường ống trong mã, giống như thế này:

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

Tiếp cận bằng cách sử dụng bản đồ


Tất nhiên nếu tất cả điều đó dường như quá lớn đối với sự nhạy cảm của bạn thì bạn có thể làm điều tương tự bằng cách sử dụng mapReduce, trông đơn giản hơn nhưng có thể sẽ chạy chậm hơn một chút.

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

Và điều đó về cơ bản dựa vào các giá trị "khóa" được phát ra nằm trong "thứ tự chỉ mục" về cách chúng xuất hiện trong mảng đầu vào.


Vì vậy, đó về cơ bản là những cách bạn duy trì thứ tự của một danh sách đầu vào với một $inđiều kiện mà bạn đã có danh sách đó theo một thứ tự xác định.


2
Câu trả lời chính xác. Đối với những người cần đến nó, một phiên bản coffeescript đây
Lawrence Jones

1
@NeilLunn Tôi đã thử cách tiếp cận bằng cách sử dụng tổng hợp, nhưng tôi nhận được id và trọng lượng. Bạn có biết cách lấy các bài viết (đối tượng) không?
Juanjo Lainez Reche

1
@NeilLunn Tôi đã thực sự làm (nó ở đây stackoverflow.com/questions/27525235/… ) Nhưng nhận xét duy nhất đang đề cập ở đây, mặc dù tôi đã kiểm tra điều này trước khi đăng câu hỏi của mình. Bạn có thể giúp tôi ở đó được không? Cảm ơn bạn!
Juanjo Lainez Reche

1
biết điều này đã cũ, nhưng tôi đã lãng phí rất nhiều thời gian để gỡ lỗi tại sao input.indexOf () không khớp với this._id. Nếu bạn chỉ trả về giá trị của Id đối tượng, bạn có thể phải chọn cú pháp này: obj.map = function () {for (var i = 0; i <input.length; i ++) {if (this. _id.equals (input [i])) {var order = i; }} release (order, {doc: this}); };
NoobSter

1
bạn có thể sử dụng "$ addFields" thay vì "$ project" nếu bạn cũng muốn có tất cả các trường gốc
Jodo

39

Một cách khác sử dụng truy vấn Tổng hợp chỉ áp dụng cho xác minh MongoDB> = 3.4 -

Tín dụng dành cho bài đăng blog tốt đẹp này .

Các tài liệu mẫu sẽ được tìm nạp theo thứ tự này -

var order = [ "David", "Charlie", "Tess" ];

Truy vấn -

var query = [
             {$match: {name: {$in: order}}},
             {$addFields: {"__order": {$indexOfArray: [order, "$name" ]}}},
             {$sort: {"__order": 1}}
            ];

var result = db.users.aggregate(query);

Một trích dẫn khác từ bài đăng giải thích các toán tử tổng hợp này được sử dụng -

Giai đoạn "$ addFields" là mới trong 3.4 và nó cho phép bạn "$ chiếu" các trường mới vào tài liệu hiện có mà không cần biết tất cả các trường hiện có khác. Biểu thức "$ indexOfArray" mới trả về vị trí của phần tử cụ thể trong một mảng nhất định.

Về cơ bản, addFieldstoán tử thêm một ordertrường mới vào mọi tài liệu khi nó tìm thấy nó và ordertrường này đại diện cho thứ tự ban đầu của mảng mà chúng tôi đã cung cấp. Sau đó, chúng tôi chỉ cần sắp xếp các tài liệu dựa trên trường này.


có cách nào để lưu trữ mảng thứ tự dưới dạng một biến trong truy vấn để chúng ta không có truy vấn lớn này của cùng một mảng hai lần nếu mảng lớn không?
Ethan SK

25

Nếu bạn không muốn sử dụng aggregate, một giải pháp khác là sử dụng findvà sau đó sắp xếp kết quả tài liệu phía máy khách bằng cách sử dụng array#sort:

Nếu các $ingiá trị là kiểu nguyên thủy như số, bạn có thể sử dụng cách tiếp cận như:

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

Nếu các $ingiá trị là kiểu không nguyên thủy như ObjectIds, thì cần phải có một cách tiếp cận khác để indexOfso sánh bằng tham chiếu trong trường hợp đó.

Nếu bạn đang sử dụng Node.js 4.x +, bạn có thể sử dụng Array#findIndexObjectID#equalsxử lý điều này bằng cách thay đổi sortchức năng thành:

docs.sort((a, b) => ids.findIndex(id => a._id.equals(id)) - 
                    ids.findIndex(id => b._id.equals(id)));

Hoặc với bất kỳ phiên bản Node.js nào, với dấu gạch dưới / 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); });
});

Làm thế nào để hàm bình đẳng biết so sánh một thuộc tính id với id 'return a.equals (id);', khiến a lưu giữ tất cả các thuộc tính được trả về cho mô hình đó?
lboyel

1
@lboyel Tôi không có ý nói nó thông minh như vậy :-), nhưng điều đó hiệu quả vì nó đang sử dụng Mongoose Document#equalsđể so sánh với trường của doc _id. Đã cập nhật để làm cho _idso sánh rõ ràng. Cam ơn vi đa hỏi.
JohnnyHK

6

Tương tự như giải pháp của JonnyHK , bạn có thể sắp xếp lại thứ tự các tài liệu được trả về từ findmáy khách của mình (nếu máy khách của bạn sử dụng JavaScript) với sự kết hợp của mapArray.prototype.findhàm trong 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);
        });
    });

});

Một số lưu ý:

  • Đoạn mã trên đang sử dụng trình điều khiển Mongo Node chứ không phải Mongoose
  • idArraymột mảng củaObjectId
  • Tôi chưa kiểm tra hiệu suất của phương pháp này so với sắp xếp, nhưng nếu bạn cần thao tác với từng mục được trả về (điều này khá phổ biến), bạn có thể thực hiện trong lệnh mapgọi lại để đơn giản hóa mã của mình.

5

Một cách dễ dàng để sắp xếp kết quả sau khi mongo trả về mảng là tạo một đối tượng có id là các khóa và sau đó ánh xạ lên các _id đã cho để trả về một mảng được sắp xếp chính xác.

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
}

1
Điều này thực hiện chính xác những gì tôi cần và đơn giản hơn nhiều so với nhận xét hàng đầu.
dyarbrough

@dyarbrough giải pháp này chỉ hoạt động cho các truy vấn tìm nạp tất cả các tài liệu (không giới hạn hoặc bỏ qua). Chú thích trên cùng phức tạp hơn nhưng phù hợp với mọi tình huống.
marian2js

4

Tôi biết câu hỏi này liên quan đến khuôn khổ Mongoose JS, nhưng câu hỏi trùng lặp là chung chung, vì vậy tôi hy vọng đăng giải pháp Python (PyMongo) ở đây là tốt.

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

3

Luôn luôn? Không bao giờ. Thứ tự luôn giống nhau: không xác định (có thể là thứ tự vật lý mà tài liệu được lưu trữ). Trừ khi bạn sắp xếp nó.


$naturalđặt hàng bình thường là hợp lý hơn là vật lý
Sammaye

1

Tôi biết đây là một chuỗi cũ, nhưng nếu bạn chỉ trả về giá trị của Id trong mảng, bạn có thể phải chọn cú pháp này. Vì dường như tôi không thể nhận được giá trị indexOf để khớp với định dạng 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});
  };

Làm thế nào để chuyển đổi mongo ObjectId .toString mà không bao gồm trình bao bọc 'ObjectId ()' - chỉ có Giá trị?


0

Bạn có thể đảm bảo đơn đặt hàng với $ hoặc điều khoản.

Vì vậy, hãy sử dụng $or: [ _ids.map(_id => ({_id}))]thay thế.


2
Giải $orpháp thay thế đã không hoạt động kể từ v2.6 .
JohnnyHK

0

Đây là một giải pháp mã sau khi kết quả được truy xuất từ ​​Mongo. Sử dụng bản đồ để lưu trữ chỉ mục và sau đó hoán đổi các giá trị.

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++
        }
    }
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.