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

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.

Related

Mongodb aggregation lookup with condition

I have two collections called 'store' and 'items' , where store keep all product records belongs to the supermarket with timestamp. And 'items' keeps records of some details about purchases.
Store Collection
{{"id": "802736816700002577966429","$date": "2021-05-05T07:02:51Z","state":"VALID"},{"id": "802736816700002577966430","$date": "2021-07-27T07:02:51Z","state":"NOT VALID"},{"id": "802736816700002577966430","$date": "2021-04-27T07:02:51Z","state":"VALID"}}
Item Collection
{{"id": "802736816700002577966429","$date": "2021-05-05T07:02:51Z"},{"id": "802736816700002577966430","$date": "2021-07-27T07:02:51Z"},{"id": "802736816700002577966430","$date": "2021-07-27T07:02:51Z"}}
The query which i want to execute is ->
Select items from Store Collection where state != VALID and timestamp<2021-05-10 and then delete corresponding items from Item Collection
Please provide Spring-boot criteria/aggregation code which is similar to
Criteria criteriaEvt = new Criteria(....);
Aggregation aggregationEvt = newAggregation(.....)

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.

update a field with the value of other field

How can I update a field with the value of other field?
For example, I want to update the field updated_at with the value of the field created_at (it's like cloning fields)
$dm->createQueryBuilder('MyBundle:MyService')
->update()
->multiple(true)
->field('updated_at')->set(CREATED_AT_ATRIBUTE)
->getQuery()
->execute();
Thanks
Option #1: Application side update
The easiest approach when using Doctrine ODM is to perform the update on the application side. So you fetch all the objects you want to update, make the necessary adjustments and flush them.
$services = $dm->createQueryBuilder('MyBundle:MyService')
->getQuery()
->execute();
foreach ($services as $service) {
$service->setUpdatedAt($service->getCreatedAt());
}
$db->flush();
$services represents the mongo cursor, which will fetch the documents as you iterate. You can set eagerCursor to true if you want to fetch all the documents from the collection at once.
Option #2: Database side update
You can also perform the update directly on the database itself. To do so however, you'll need to create the query yourself as the query builder doesn't support this functionality.
// Get MongoDB instance from DocumentManager
$mongo = $dm->getDocumentDatabase('Fully/Qualified/Class/Name')
->getMongoDB();
// Iterate over the collection and update each document
$mongo->execute('
db.MyServiceCollection.find().forEach(function (item) {
db.MyServiceCollection.update(
{_id: item._id},
{$set: {updated_at: item.created_at}}
);
});
');
The function used in forEach is just a regular javascript function, so you can insert some more logic in there if you need more control over what you update.

Search for ObjectId of a document: pymongo

I want to access a document in collection by 'name' attribute for getting its ObjectId so that i can insert that unique objectid to other document for reference.
cursorObject = db.collectionIngredient.find({'name': 'sugar'})
I want _id field of cursorObject.
cursorObject.'_id' or cursorObject._id not working.
I have tried __getitem__, __getattribute__ and so much internet surfing but couldn't able to find a way.
Please help
First, as #jjmartinez pointed out, find returns a cursor, which you need to iterate over, in order to get hold of the documents returned by your query. The _id field belongs to the documents, not the cursor.
I'm guessing that in your case the name is unique, so you can avoid cursor/iterating if you use find_one instead of find. Then you get the document directly.
Then, to access the _id, you just need a standard dict-item access:
id = doc['_id']
So we get:
ingredient = db.collectionIngredient.find_one({'name': 'sugar'})
if ingredient is not None:
id = ingredient['_id']
else:
id = None
When you do cursorObject = db.collectionIngredient.find({'name': 'sugar'}) you have a collection of documents, not a single element. So you need to explore all the collection. You need to iterate inside the cursor:
try:
cursorObject = db.collectionIngredient.find({'name': 'sugar'})
except:
print "Unexpected error:", sys.exc_info()[0]
for doc in cursorObject:
print doc
Here you have the Pymongo Tutorial

MongoDb replacing a document and inserting when non-existant

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));
?>