Search code examples
phpmongodbphp-mongodbdatabase

mongodb php finding a subobject and removing it, $inc other subobject


I'm trying to delete the subobject 'apples' from my documents and update the 'fruitInventory' property, decrease by the amount of apples.

I'm confused on how to proceed, should I use dot notation or do a full text search for apples? I don't know if this matters but you can assume apples will always be in field 1.

// Document 1 
{
    "1": {
        "apples": 3,
        "fruitInventory": 21,
        "oranges": 12,
        "kiwis": 3,
        "lemons": 3
    },
    "2": {
        "bananas": 4,
        "fruitInventory": 12,
        "oranges": 8,
    },
    "_id": "1"
}

// Document 2
{
    "1": {
        "apples": 5,
        "fruitInventory": 10,
        "oranges": 2,
        "pears": 3
    },
    "2": {
        "bananas": 4,
        "fruitInventory": 6,
        "cherries": 2,
    },
    "_id": "2"
}

Result should be like this:

// Document 1 
{
    "1": {
        "fruitInventory": 18,
        "oranges": 12,
        "kiwis": 3,
        "lemons": "3"
    },
    "2": {
        "bananas": 4,
        "fruitInventory": 12,
        "oranges": 8,
    },
    "_id": "1"
}

// Document 2
{
    "1": {
        "fruitInventory": 5,
        "oranges": "2",
        "pears": "3"
    },
    "2": {
        "bananas": 4,
        "fruitInventory": 6,
        "cherries": 2,
    },
    "_id": "2"
}

Thanks in advance for your help.


Solution

  • In the MongoDB query language there is no way to update a document using information from the document. In other words, there is no way to increment one property with a value from another property. To be specific: there is no way to increment the fruitInventory by X where X is the value of the apples property as an atomic operation.

    The way I would implement this, while avoiding races, is with two findAndModify operations (seen here in Mongo shell syntax):

    var fruit = db.fruit.findAndModify({
     query: {locked: {$ne: true}, "1.apples": {$exists: true}},
     update: {$set: {locked: true}}
    });
    
    var fruitInventory = fruit["1"]["fruitInventory"];
    var apples = fruit["1"]["apples"];
    
    db.fruit.findAndModify({
      query: {"_id": fruit["_id"]},
      update: {
        $set: {"1.fruitInventory": fruitInventory + apples}, 
        $unset: {locked: false, "1.apples": true}
      }
    });
    

    What happens is this: first I find a document which is not locked (more on this later), and has an 1.apples property. The document is returned and updated to get a locked property as a single atomic operation. Since the document now has a locked property it will not be found by the same query, so this query can be run multiple times in parallel without any risk of modifying the same document twice.

    I then extract the 1.fruitInventory and 1.apples, to simplify the next expression.

    The second findAndModify updates the 1.fruitInventory property with the new sum, and unsets the 1.apples property, as well as the locked property (to return the document to its previous, unlocked, state).

    To update all documents you will have to run this code over and over again until the first findAndModify returns null (meaning there are no documents matching the query).