rottenoats rottenoats - 1 year ago 61
JSON Question

Update object if specific key exists otherwise $addToSet

Question:



How do I update an object within a list based on the specific key within that object?

Description:



Say I have the following object in my mongodb:

{
"keysteps":[
{"timecode":"01:00:00", "title": "Chapter 1"}
]
}


And say I would like to update "keysteps" with a list of keysteps (aka: "chapters"), how would I go about making sure that if a timecode already exists, that I update the object instead of creating a new one?

If I update my mongodb with these keysteps:

{
"keysteps":[
{"timecode":"01:00:00", "title": "Chapter 1 - edited"}
]
}


Here is the output I get:

keysteps": [
{
"timecode": "01:00:00",
"title": "Chapter 1"
},
{
"timecode": "01:00:00",
"title": "Chapter 1 - edited"
}
]


Instead of updating keysteps, it added a new object within the keysteps list. If I had sent the same timecode object (same timecode and title) it would not have created a new one.

What I've done:



This is a snippet from my Symfony controller that I use to update:

class VideoToolsController extends Controller
{
//More code

public function keystepsUpdateAction(Request $request, int $video_id){
//If the video id already exists, throw an error

$keystepsS = $this->get('video_tools.keysteps');
$exists = $keystepsS->exists($video_id);

//Return immediately if keysteps doesn't exist.
if(!$exists)
return JsonResponseService::getErrorBadRequest("Keystep does not exist.");

$keysteps = json_decode($request->getContent(), true);
//die(var_dump($data));
$form = $this->createForm(KeystepsType::class);
$form->submit($keysteps);
//return new JsonResponse($data);
//Return bad request if the form is not valid.
if(!$form->isValid())
return JsonResponseService::getErrorBadRequest("Keysteps are not valid, try again.");

try {
$res = $keystepsS->update($video_id, $keysteps['keysteps']);
return JsonResponseService::getSuccess($res, "Updated keysteps.");

} catch(Exception $exception){
return JsonResponseService::getErrorBadRequest($exception->getMessage(), $keysteps);
}
}
//More code
}


This is a snippet from my Php service that I use to update:

class KeystepsService {

//More code

//TODO: Make timecode UNIQUE
public function update($video_id, $keysteps) : int {

$updatedOne = $this->mongoCollection->updateOne(
['video_id' => $video_id ],
['$addToSet' => [ "keysteps" => [ '$each' => $keysteps ]]]
);

return $updatedOne->getModifiedCount();
}

//More code
}


mongodb/mongo-php-library:



mongodb/mongo-php-library

Feel free to give me advice on how to word the title/question, I feel as if it's not on point.

Answer Source

As you have discovered, the $addToSet operator is not the right thing to use here. This is by design since the "set" is of the "objects" in the array, and if you give a different combination of values in the keys in any way then this is a "new" object and is added.

So the correct way to handle this is with "two" operations:

  1. Attempt to find the item where it does exist in the array by key and then update it with $set
  2. Attempt to $push the item to the array if the key does not exist.

Clearly you don't want to be writing and waiting for a response from the database for multiple operations, so instead you use the bulkWrite() method to send both at once:

$this->mongoCollection->bulkWrite([
  [ 'updateOne' => [ 
    [ 'video_id' => $video_id, 'keysteps.timecode' => $keysteps[0]['timecode'] ],
    [ '$set' => [ 'keysteps.$.title' => $keysteps[0]['title'] ] ]
  ]],
  [ 'updateOne' => [
    [ 'video_id' => $video_id, 'keysteps.timecode' => [ '$ne' => $keysteps[0]['timecode'] ] ],
    [ '$push' => [ 'keysteps' => $keysteps[0] ]
  ]]
])

Note that whilst still keeping your array object structure but deliberately not using $each here due to the nature of this two step operation. If your input can have more than one array item in it, then you would loop the items and create the "pair" of operations pulling the array values out by index.

So the 0 index usage here is just a placeholder for the index variable you would use to drive this in supporting multiple array items within input. You also would just generate the "internal" documents for the "operations" rather than actually issue the bulkWrite() several times. The whole point of this method is after all to call it only once.

Edit working solution:

public function update($video_id, $keysteps) : int {

    $modifiedData = 0;
    foreach($keysteps as $keystep){
        $bulkUpdate =  $this->mongoCollection->bulkWrite([
            [ 'updateOne' => [
                [ 'video_id' => $video_id, 'keysteps.timecode' => $keystep['timecode'] ],
                [ '$set' => [ 'keysteps.$.title' => $keystep['title'] ] ]
            ]],
            [ 'updateOne' => [
                [ 'video_id' => $video_id, 'keysteps.timecode' => [ '$ne' => $keystep['timecode'] ] ],
                [ '$push' => [ 'keysteps' => $keystep ]
                ]]
            ]]);
        $modifiedData += $bulkUpdate->getModifiedCount();
    }

    return $modifiedData;
}
Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download