MongoDb replacing a document and inserting when non-existant - mongodb

I want to replace a document when this already exists and if it doesn't I want it inserted.
How can I do that in mongoDb?
I need something like this, but in one query:
find by a "where statement"
if exists, replace whole document
else, insert
Thank you!

you could also use the save operation, that is much faster than the update (x70 faster from my tests), and is adapt to your purpose, but in case remember to give in input the whole document

Use collection update.
In the example below, the first update call will "insert or replace" the document (including name field from the query). In the second the update call will insert the document or just update Joe's job leaving the rest of the document intact. The difference is the "$set" operation.
<?php
$c->update(
array("name" => "joe"),
array("username" => "joe312", "job" => "Codemonkey"),
array("upsert" => true));
$c->update(
array("name" => "joe"),
array("$set" => array("job" => "Bartender")),
array("upsert" => true));
?>

Related

How can I upsert a record and array element at the same time?

That is meant to be read as a dual upsert operation, upsert the document then the array element.
So MongoDB is a denormalized store for me (we're event sourced) and one of the things I'm trying to deal with is the concurrent nature of that. The problem is this:
Events can come in out of order, so each update to the database need to be an upsert.
I need to be able to not only upsert the parent document but an element in an array property of that document.
For example:
If the document doesn't exist, create it. All events in this stream have the document's ID but only part of the information depending on the event.
If the document does exist, then update it. This is the easy part. The update command is just written as UpdateOneAsync and as an upsert.
If the event is actually to update a list, then that list element needs to be upserted. So if the document doesn't exist, it needs to be created and the list item will be upserted (resulting in an insert); if the document does exist, then we need to find the element and update it as an upsert, so if the element exists then it is updated otherwise it is inserted.
If at all possible, having it execute as a single atomic operation would be ideal, but if it can only be done in multiple steps, then so be it. I'm getting a number of mixed examples on the net due to the large change in the 2.x driver. Not sure what I'm looking for beyond the UpdateOneAsync. Currently using 2.4.x. Explained examples would be appreciated. TIA
Note:
Reiterating that this is a question regarding the MongoDB C# driver 2.4.x
Took some tinkering, but I got it.
var notificationData = new NotificationData
{
ReferenceId = e.ReferenceId,
NotificationId = e.NotificationId,
DeliveredDateUtc = e.SentDate.DateTime
};
var matchDocument = Builders<SurveyData>.Filter.Eq(s => s.SurveyId, e.EntityId);
// first upsert the document to make sure that you have a collection to write to
var surveyUpsert = new UpdateOneModel<SurveyData>(
matchDocument,
Builders<SurveyData>.Update
.SetOnInsert(f => f.SurveyId, e.EntityId)
.SetOnInsert(f => f.Notifications, new List<NotificationData>())){ IsUpsert = true};
// then push a new element if none of the existing elements match
var noMatchReferenceId = Builders<SurveyData>.Filter
.Not(Builders<SurveyData>.Filter.ElemMatch(s => s.Notifications, n => n.ReferenceId.Equals(e.ReferenceId)));
var insertNewNotification = new UpdateOneModel<SurveyData>(
matchDocument & noMatchReferenceId,
Builders<SurveyData>.Update
.Push(s => s.Notifications, notificationData));
// then update the element that does match the reference ID (if any)
var matchReferenceId = Builders<SurveyData>.Filter
.ElemMatch(s => s.Notifications, Builders<NotificationData>.Filter.Eq(n => n.ReferenceId, notificationData.ReferenceId));
var updateExistingNotification = new UpdateOneModel<SurveyData>(
matchDocument & matchReferenceId,
Builders<SurveyData>.Update
// apparently the mongo C# driver will convert any negative index into an index symbol ('$')
.Set(s => s.Notifications[-1].NotificationId, e.NotificationId)
.Set(s => s.Notifications[-1].DeliveredDateUtc, notificationData.DeliveredDateUtc));
// execute these as a batch and in order
var result = await _surveyRepository.DatabaseCollection
.BulkWriteAsync(
new []{ surveyUpsert, insertNewNotification, updateExistingNotification },
new BulkWriteOptions { IsOrdered = true })
.ConfigureAwait(false);
The post linked as being a dupe was absolutely helpful, but it was not the answer. There were a few things that needed to be discovered.
The 'second statement' in the linked example didn't work
correctly, at least when translated literally. To get it to work, I had to match on the
element and then invert the logic by wrapping it in the Not() filter.
In order to use 'this index' on the match, you have to use a
negative index on the array. As it turns out, the C# driver will
convert any negative index to the '$' character when the query is
rendered.
In order to ensure they are run in order, you must include bulk write
options with IsOrdered set to true.

Why would this upsert fail with a duplicate id exception?

This one keep cropping up from time to time. I have the operation done as an upsert, but every so often, the service crashes because it runs into this error and I don't understand how it's even possible. I try to do the upsert using the SurveyId as the key on which to match:
await _surveyRepository.DatabaseCollection.UpdateOneAsync(
Builders<SurveyData>.Filter.Eq(survey => survey.SurveyId, surveyData.SurveyId),
Builders<SurveyData>.Update
.Set(survey => survey.SurveyLink, surveyData.SurveyLink)
.Set(survey => survey.ClientId, surveyData.ClientId)
.Set(survey => survey.CustomerFirstName, surveyData.CustomerFirstName)
.Set(survey => survey.CustomerLastName, surveyData.CustomerLastName)
.Set(survey => survey.SurveyGenerationDateUtc, surveyData.SurveyGenerationDateUtc)
.Set(survey => survey.PortalUserId, surveyData.PortalUserId)
.Set(survey => survey.PortalUserFirst, surveyData.PortalUserFirst)
.Set(survey => survey.PortalUserLast, surveyData.PortalUserLast)
.Set(survey => survey.Tags, surveyData.Tags),
new UpdateOptions { IsUpsert = true })
.ConfigureAwait(false);
And I'll occasionally get this error:
Message: A write operation resulted in an error. E11000 duplicate
key error collection: surveys.surveys index: SurveyId dup key: { :
"" }
The id is a string representation of a Guid and is set to unique in mongo.
So why would this happen? It is my understanding that if it finds the key, it'll update the defined properties, and if not, it'll insert. Is that not correct? Because, that is the effect that I need.
C# driver version is 2.4.1.18
This happens because according to this Jira ticket:
During an update with upsert:true option, two (or more) threads may attempt an upsert operation using the same query predicate and, upon not finding a match, the threads will attempt to insert a new document. Both inserts will (and should) succeed, unless the second causes a unique constraint violation.
It is my understanding that if it finds the key, it'll update the
defined properties, and if not, it'll insert. Is that not correct
Yes that's what upsert does. And newly inserted document will contain all fields from criteria part(in your case surveyId) as well as update modification part(all other specified fields) of your update query.
You need to set upsert=false in your query. Then it will only update documents with matching criteria, and update will fail if no match is found.

Doctrine ODM(mongo) - upsert embeded document if not exist

A mongo document exists which links Users and Cars. It contains the following fields:
User.userId
User.cars[]
User.updated
User.created
User.cars is an array of embedded documents. A query needs to be written to only insert a Car to this field if it does not already contain a Car with id $carId
The following query will create a new record each time for $userId and $carId. What it should do is either not insert a new record or update the value for driven.
$qb
->findAndUpdate()
->upsert(true)
->field('userId')->equals($userId)
->field('cars.id')->notEqual($carId)
->field('cars')->addToSet([
'id' => $carId,
'driven' => new \DateTime(),
])
->field('updated')->set(new \DateTime())
->field('created')->setOnInsert(new \DateTime())
->limit(1)
;
return $qb->getQuery()->execute();
Removing notEqual($carId) from the query will always insert the record into User.cars, which is also not desired.
The end result should be that User.cars only contains one record for each carId.
If you don't need to search the array of cars later you could store it as an object instead and use $carId as key:
$qb
->findAndUpdate()
->upsert(true)
->field('userId')->equals($userId)
->field("cars.$carId")->set([
'id' => $carId,
'driven' => new \DateTime(),
])
->field('updated')->set(new \DateTime())
->field('created')->setOnInsert(new \DateTime())
->limit(1)
;
return $qb->getQuery()->execute();
If cars are mapped in the document as EmbedMany you may need to change your collection's strategy to either set or atomicSet (I'd suggest the latter) as otherwise ODM will reindex your array each time it's saved.
Once again, this solution has its downsides which can be considered serious depending on how you want to use your data but it solves the problem in question.
Offhand, ->limit(1) is superfluous as ->findAndUpdate() is making a query run findAndModify command which by its nature operates on single document.

How to unset field from Document

I have one document in which I want to remove one field. I got answer from Here. But, It is for core PHP code and I want in CakePHP how to unset field from document.
I tried following code in CakePHP.
$this->MyModal->id = $id;
$this->MyModal->updateAll(
array('myField' => array('$exists' => true)),
array('unset' => array('myField' => true))
);
Edit
I also tried
$this->MyModal->updateAll(
array('$unset' => array('myField' => 1))
);
But it does not work.How can unset field?
It looks like you're using wrong the update all method. First you must provide an array with the new values you want for your fields and then you should pass an array with the search criteria. So in order for it to work you could write something like
$conditions=array('myField'=>array('$exists'=>true));
$this->MyModal->updateAll(
array('$unset'=>array('myfield'=>1)),
$conditions//array of search conditions
)

How to delete all documents with safe mode?

remove() method without any argument removes all documents inside the collection.
$this->db->$collection->remove();
But how to remove all documents with safe mode?
What should be the first argument of remove?
passing array('safe' => true) as first argument doesn't delete all document because it's treated like a filter with key 'safe'.
$this->db->$collection->remove(array('safe' => true));
You need to pass that as second parameter in the remove call :
http://php.net/manual/en/mongocollection.remove.php
$this->db->$collection->remove(array(),array('safe' => true));