Rename a sub-document field within an Array

时间:2015-07-28 22:21:06

标签: mongodb mongoose mongodb-query

Considering the document below how can I rename 'techId1' to 'techId'. I've tried different ways and can't get it to work.

{
        "_id" : ObjectId("55840f49e0b"),
        "__v" : 0,
        "accessCard" : "123456789",
        "checkouts" : [ 
            {
                "user" : ObjectId("5571e7619f"),
                "_id" : ObjectId("55840f49e0bf"),
                "date" : ISODate("2015-06-19T12:45:52.339Z"),
                "techId1" : ObjectId("553d9cbcaf")
            }, 
            {
                "user" : ObjectId("5571e7619f15"),
                "_id" : ObjectId("55880e8ee0bf"),
                "date" : ISODate("2015-06-22T13:01:51.672Z"),
                "techId1" : ObjectId("55b7db39989")
            }
        ],
        "created" : ISODate("2015-06-19T12:47:05.422Z"),
        "date" : ISODate("2015-06-19T12:45:52.339Z"),
        "location" : ObjectId("55743c8ddbda"),
        "model" : "model1",
        "order" : ObjectId("55840f49e0bf"),
        "rid" : "987654321",
        "serialNumber" : "AHSJSHSKSK",
        "user" : ObjectId("5571e7619f1"),
        "techId" : ObjectId("55b7db399")
    }

In mongo console I tried which gives me ok but nothing is actually updated.

collection.update({"checkouts._id":ObjectId("55840f49e0b")},{ $rename: { "techId1": "techId" } });

I also tried this which gives me an error. "cannot use the part (checkouts of checkouts.techId1) to traverse the element"

collection.update({"checkouts._id":ObjectId("55856609e0b")},{ $rename: { "checkouts.techId1": "checkouts.techId" } })

In mongoose I have tried the following.

collection.findByIdAndUpdate(id, { $rename: { "checkouts.techId1": "checkouts.techId" } }, function (err, data) {});

and

collection.update({'checkouts._id': n1._id}, { $rename: { "checkouts.$.techId1": "checkouts.$.techId" } }, function (err, data) {});

Thanks in advance.

2 个答案:

答案 0 :(得分:11)

You were close at the end, but there are a few things missing. You cannot $rename when using the positional operator, instead you need to $set the new name and $unset the old one. But there is another restriction here as they will both belong to "checkouts" as a parent path in that you cannot do both at the same time.

The other core line in your question is "traverse the element" and that is the one thing you cannot do in updating "all" of the array elements at once. Well, not safely and without possibly overwriting new data coming in anyway.

What you need to do is "iterate" each document and similarly iterate each array member in order to "safely" update. You cannot really iterate just the document and "save" the whole array back with alterations. Certainly not in the case where anything else is actively using the data.

I personally would run this sort of operation in the MongoDB shell if you can, as it is a "one off" ( hopefully ) thing and this saves the overhead of writing other API code. Also we're using the Bulk Operations API here to make this as efficient as possible. With mongoose it takes a bit more digging to implement, but still can be done. But here is the shell listing:

var bulk = db.collection.initializeOrderedBulkOp(),
    count = 0;

db.collection.find({ "checkouts.techId1": { "$exists": true } }).forEach(function(doc) {
    doc.checkouts.forEach(function(checkout) {
        if ( checkout.hasOwnProperty("techId1") ) { 
            bulk.find({ "_id": doc._id, "checkouts._id": checkout._id }).updateOne({
                "$set": { "checkouts.$.techId": checkout.techId1 }
            });
            bulk.find({ "_id": doc._id, "checkouts._id": checkout._id }).updateOne({
                "$unset": { "checkouts.$.techId1": 1 }
            });
            count += 2;

            if ( count % 500 == 0 ) {
                bulk.execute();
                bulk = db.collection.initializeOrderedBulkOp();
            }
        }
    });
});

if ( count % 500 !== 0 ) 
    bulk.execute();

Since the $set and $unset operations are happening in pairs, we are keeping the total batch size to 1000 operations per execution just to keep memory usage on the client down.

The loop simply looks for documents where the field to be renamed "exists" and then iterates each array element of each document and commits the two changes. As Bulk Operations, these are not sent to the server until the .execute() is called, where also a single response is returned for each call. This saves a lot of traffic.

If you insist on coding with mongoose. Be aware that a .collection acessor is required to get to the Bulk API methods from the core driver, like this:

var bulk = Model.collection.inititializeOrderedBulkOp();

And the only thing that sends to the server is the .execute() method, so this is your only execution callback:

bulk.exectute(function(err,response) {
    // code body and async iterator callback here
});

And use async flow control instead of .forEach() such as async.each.

Also, if you do that, then be aware that as a raw driver method not governed by mongoose, you do not get the same database connection awareness as you do with mongoose methods. Unless you know for sure the database connection is already established, it is safter to put this code within an event callback for the server connection:

mongoose.connection.on("connect",function(err) {
    // body of code
});

But otherwise those are the only real ( apart from call syntax ) alterations you really need.

答案 1 :(得分:0)

这对我有用,我创建了这个查询来执行这个过程并分享它,(虽然我知道这不是最优化的方式):

首先,创建一个 aggregate,即 (1) $match 具有 checkouts 数组字段的文档,其中 techId1 作为每个子文档的键之一。 (2) $unwind checkouts 字段(从输入文档中解构数组字段,为每个元素输出一个文档),(3) 添加 techId 字段(带有 {{3 }}), (4) $addFields 旧的 techId1 字段,(5) $unset _id 的文档再次将 checkout 子文档分组通过其 _id,以及 (6) 将这些聚合的结果写入 temporal 集合(使用 $group)。

const collection = 'yourCollection'

db[collection].aggregate([
    {
        $match: {
            'checkouts.techId1': { '$exists': true }
        }
    },
    {
        $unwind: {
            path: '$checkouts'
        }
    },
    {
        $addFields: {
            'checkouts.techId': '$checkouts.techId1'
        }
    },
    {
        $project: {
            'checkouts.techId1': 0
        }
    },
    {
        $group: {
            '_id': '$_id',
            'checkouts': { $push: { 'techId': '$checkouts.techId' } }
        }
    },
    {
        $out: 'temporal'
    }
])

然后,您可以将此 temporal 集合中的另一个聚合$out 带有修改过的 checkouts 字段的文档添加到您的原始集合中。

db.temporal.aggregate([
    {
        $merge: {
            into: collection,
            on: "_id",
            whenMatched:"merge",
            whenNotMatched: "insert"
        }
    }
])