简体   繁体   中英

Using the Mongodb findAndModify on subdocuments

I have a collection who's documents contain an array of sub-documents called "labels". I'd like to be able to findAndModify on the label level. The purpose is to lock each label so the user interface will not allow more than one user to view each label at a time without locking the entire document. At the moment I'm simply attempting to add a field called "lock" to each label as it is hit by my findAndModify query.

The query is currently partially working. It will find a document that has at least one label that does not contain a "lock" field. However, it only adds the lock to the first label, even if the first label is already locked. Here is my current query:

db.manual_ncsc_test.findAndModify({query: {$and: [{"labels.x": 201}, {"labels": {$elemMatch: {$exists: {"lock": false}}}}]}, sort:{"labels.lock": -1}, update: {$set: {"labels.$.lock": "TEST!!!!"}}, upsert: false})

The first part of the $and is simply to make sure I test on the same document each time. I then use $elemMatch to check each label for a lock.

I tried various sorts in an attempt to fix my problem but I believe it is just sorting at the main document level, not the labels themselves.

You can see I am using the positional operator to add the lock to the matched label. This is what really confuses me, the positional operator should only be hitting labels that match the query, but it modifies the first label in the array every time, whether it has a lock or not(confirmed by changing the text for the $set ).

EDIT: To prevent any other incorrect assumptions; These label objects are served to the user with no input. The user only interacts with a label after it has been chosen by the node.js server, so the only requirements are that it is a label, and that it is not already being examined by another user(locked). The server has no way of knowing anything about a label before the findAndModify is run, so searching for ids and other unique info is not possible.

Indeed you seem to have a miscomprehension of how the "sort" option is applied with .findAndModify() along with some other usage problems.

The purpose of "sort" is to be applied when your "query" part of the statement matches more than one document. In this case, as .findAndModify() acts on only one document at a time, the "sort" is used to determine "which document" will be processed with the "update" or other operation such as "remove". This does not sort arrays, as indeed none of the standard operations do, with the exception of the $sort modifier for updates and operations that the aggregation framework can do in queries.

You main problem with matching a specific "index" to your array is that you need to compare "multiple" attributes of the array element in order to determine the positional $ match. The $elemMatch operator does this by "querying" each element for the provided conditions.

For the sort of thing you are describing, what you want is a "unique identifier" on the array elements you are intending to alter or edit. I'll give a sample here using ObjectId() values for each array element:

{
    "_id" : ObjectId("53cf1b1c71c5e451279fce3b"),
    "labels" : [
            {
                    "_id" : ObjectId("53cf1b1c71c5e451279fce37"),
                    "name" : "A"
            },
            {
                    "_id" : ObjectId("53cf1b1c71c5e451279fce38"),
                    "name" : "B"
            },
            {
                    "_id" : ObjectId("53cf1b1c71c5e451279fce39"),
                    "name" : "C"
            },
            {
                    "_id" : ObjectId("53cf1b1c71c5e451279fce3a"),
                    "name" : "D"
            }
    ]
}

To "fetch" the data for the current modifier, issue your .findAndModify() with the assertion that both the element "_id" must match and that there is no locked attribute present for that element using $exists . In the "update" portion you are going to $set the locked attribute:

db.sample.findAndModify({
    "query": {
        "_id": ObjectId("53cf1b1c71c5e451279fce3b"),
        "labels": {
            "$elemMatch": {           
                "_id": ObjectId("53cf1b1c71c5e451279fce38"),
                "locked": { "$exists": false }
            }
        }
    },
    "update": {
        "$set": { "labels.$.locked": true },
    },
    "new": true
});

You could optionally project the matched field using the "fields" option, this may not be part of the actual driver implementation in some languages though and matching the array element to the arguments in your query is trivial. But generally, the document now looks like this:

{
    "_id" : ObjectId("53cf1b1c71c5e451279fce3b"),
    "labels" : [
            {
                    "_id" : ObjectId("53cf1b1c71c5e451279fce37"),
                    "name" : "A"
            },
            {
                    "_id" : ObjectId("53cf1b1c71c5e451279fce38"),
                    "name" : "B",
                    "locked": true
            },
            {
                    "_id" : ObjectId("53cf1b1c71c5e451279fce39"),
                    "name" : "C"
            },
            {
                    "_id" : ObjectId("53cf1b1c71c5e451279fce3a"),
                    "name" : "D"
            }
    ]
}

As you would see, any subsequent query with the same parameters will not actually match any document as the selected array element actually has a "locked" attribute. This stops other processes from accessing the same "label" for edit at the same time.

Any other array element can be returned however as well as obtaining the "lock" that is required. The $elemMatch here makes sure that both conditions are met and selects the correct element, where of course only one will ever match the query given. So the following would allow another process to "lock" the last element in the array for it's edit:

db.sample.findAndModify({
    "query": {
        "_id": ObjectId("53cf1b1c71c5e451279fce3b"),
        "labels": {
            "$elemMatch": {           
                "_id": ObjectId("53cf1b1c71c5e451279fce3a"),
                "locked": { "$exists": false }
            }
        }
    },
    "update": {
        "$set": { "labels.$.locked": true },
    },
    "new": true
});

Now in the database and to the returned document "two" of the elements are currently "locked":

{
    "_id" : ObjectId("53cf1b1c71c5e451279fce3b"),
    "labels" : [
            {
                    "_id" : ObjectId("53cf1b1c71c5e451279fce37"),
                    "name" : "A"
            },
            {
                    "_id" : ObjectId("53cf1b1c71c5e451279fce38"),
                    "name" : "B",
                    "locked": true
            },
            {
                    "_id" : ObjectId("53cf1b1c71c5e451279fce39"),
                    "name" : "C"
            },
            {
                    "_id" : ObjectId("53cf1b1c71c5e451279fce3a"),
                    "name" : "D",
                    "locked": true
            }
    ]
}

And when you want to modify the document, issue that "locked" is true for the element and then $unset the locked attribute as well as issuing your changed value:

db.sample.findAndModify({
    "query": {
        "_id": ObjectId("53cf1b1c71c5e451279fce3b"),
        "labels": {
            "$elemMatch": {           
                "_id": ObjectId("53cf1b1c71c5e451279fce38"),
                "locked": { "$exists": true }
            }
        }
    },
    "update": {
        "$unset": { "labels.$.locked": true },
        "$set": { "labels.$.name": "C" }
    },
    "new": true
});

So this now modifies array element in the document that this process had the "lock" on and does not affect other locked elements. Which leaves the other process to issue it's own update in order to change that element:

{
    "_id" : ObjectId("53cf1b1c71c5e451279fce3b"),
    "labels" : [
            {
                    "_id" : ObjectId("53cf1b1c71c5e451279fce37"),
                    "name" : "A"
            },
            {
                    "_id" : ObjectId("53cf1b1c71c5e451279fce38"),
                    "name" : "C"
            },
            {
                    "_id" : ObjectId("53cf1b1c71c5e451279fce39"),
                    "name" : "C"
            },
            {
                    "_id" : ObjectId("53cf1b1c71c5e451279fce3a"),
                    "name" : "D",
                    "locked": true
            }
    ]
}

That is the general way to get the sort of atomic updates you want while making sure that no other update statement could modify the same element while your edit is in progress.

For bonus points, add a timestamp attribute as well or simply set the locked attribute to the current time. This allows you to periodically check and "unlock" any edits that may have "expired" or otherwise did not complete and unlock the element by itself.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM