I have a "Payee" BsonDocument like this:
{
"Token" : "0b21ae960f25c6357286ce6c206bdef2",
"LastAccessed" : ISODate("2012-07-11T02:14:59.94Z"),
"Firstname" : "John",
"Lastname" : "Smith",
"PayrollInfo" : [{
"Tag" : "EARNINGS",
"Value" : "744.11",
}, {
"Tag" : "DEDUCTIONS",
"Value" : "70.01",
}],
},
"Status" : "1",
"_id" : ObjectId("4fc263158db2b88f762f1aa5")
}
I retrieve this document based on the Payee _id.
var collection = database.GetCollection("Payee");
var query = Query.EQ("_id", _id);
var bDoc = collection.FindOne(query);
Then, at various times I need to update a specific object inside the PayrollInfo array. So I search for the object with appropriate "Tag" inside the array and update the "Value" into the database. I use the following logic to do this:
var bsonPayrollInfo = bDoc["PayrollInfo", null];
if (bsonPayrollInfo != null)
{
var ArrayOfPayrollInfoObjects = bsonPayrollInfo.AsBsonArray;
for (int i = 0; i < ArrayOfPayrollInfoObjects.Count; i++)
{
var bInnerDoc = ArrayOfPayrollInfoObjects[i].AsBsonDocument;
if (bInnerDoc != null)
{
if (bInnerDoc["Tag"] == "EARNINGS")
{
//update here
var update = Update
.Set("PayrollInfo."+ i.ToString() + ".Value", 744.11)
collection.FindAndModify(query, null, update);
bUpdateData = true;
break;
}
}
}
}
if (!bUpdateData)
{
//Use Update.Push. This works fine and is not relevant to the question.
}
All this code works fine, but I think I am being cumbersome in achieving the result. Is there a more concise way of doing this? Essentially, I am trying to find a better way of updating an object inside of an array in a BsonDocument.
Mongo has a positional operator that will let you operate on the matched value in an array. The syntax is: field1.$.field2
Here's an example of how you'd use it from the Mongo shell:
db.dots.insert({tags: [{name: "beer", count: 2}, {name: "nuts", count: 3}]})
db.dots.update({"tags.name": "beer"}, {$inc: {"tags.$.count" : 1}})
result = db.dots.findOne()
{ "_id" : ObjectId("50078284ea80325278ff0c63"), "tags" : [ { "name" : "beer", "count" : 3 }, { "name" : "nuts", "count" : 3 } ] }
Putting my answer here in case it helps you. Based on #MrKurt's answer (thank you!), here is what I did to rework the code.
var collection = database.GetCollection("Payee");
var query = Query.EQ("_id", _id);
if (collection.Count(query) > 0)
{
//Found the Payee. Let's save his/her Tag for EARNINGS
UpdateBuilder update = null;
//Check if this Payee already has any EARNINGS Info saved.
//If so, we need to update that.
query = Query.And(query,
Query.EQ("PayrollInfo.Tag", "EARNINGS"));
//Update will be written based on whether we find the Tag:EARNINGS element in the PayrollInfo array
if (collection.Count(query) > 0)
{
//There is already an element in the PayrollInfo for EARNINGS
//Just update that element
update = Update
.Set("PayrollInfo.$.Value", "744.11");
}
else
{
//This user does not have any prior EARNINGS data. Add it to the user record
query = Query.EQ("_id", _id);
//Add a new element in the Array for PayrollInfo to store the EARNINGS data
update = Update.Push("PayrollInfo",
new BsonDocument {{"Tag", "EARNINGS"}, {"Value", "744.11"}}
);
}
//Run the update
collection.FindAndModify(query, null, update);
}
It doesn't look any lesser than my original code, but it is much more intuitive, and I got to learn a lot about positional operators!
Related
at the moment my notification documents has an events property which is an array of event. Each event has a status and a date. When querying notifications, it needs to check if the top status is opened.
Valid object where most recent event status is opened -
{
"subject" : "Hello there",
"events" : [
{
"status" : "opened",
"date" : 2020-01-02 17:35:31.229Z
},
{
"status" : "clicked",
"date" : 2020-01-01 17:35:31.229Z
},
]
}
Invalid object where status isn't most recent
{
"subject" : "Hello there",
"events" : [
{
"status" : "opened",
"date" : 2020-01-01 17:35:31.229Z
},
{
"status" : "clicked",
"date" : 2020-01-02 17:35:31.229Z
},
]
}
At the moment I have the query that can check if any event has the status opened, but I'm unsure how to query only the top 1 and sorted by the dates of a nested query. Any help would be greatly appreciated.
var filter = Builders<Notification>.Filter.Empty;
filter &= Builders<Notification>.Filter.Regex("events.event", new BsonRegularExpression(searchString, "i"));
var results = await collection.FindSync(filter, findOptions).ToListAsync();
In order to get only the latest event you can use $reduce to iterate over the events and compare each one to the temporarily latest:
db.collection.aggregate([
{
$addFields: {
latestEvent: {
$reduce: {
input: "$events",
initialValue: {status: null, date: 0},
in: {
$mergeObjects: [
"$$value",
{
$cond: [
{
$gt: [{$toDate: "$$this.date"}, {$toDate: "$$this.value"}]
},
"$$this",
"$$value"
]
}
]
}
}
}
}
}
])
See how it works on the playground example
for multiple documents, the result return only correct documents
example
db.collection.aggregate([{
$addFields: {
lastevent: {
$filter: {
input: '$events',
as: 'element',
cond: {$eq: ['$$element.date',{$max: '$events.date'}]}
}
}
}
}, {
$match: {
'lastevent.status': 'opened'
}
}])
I am a fan of not using an axe for everything, even if it is a good one :)
So i take it the events being disorderly is a rare thing, so we don't need to spend a lot of resources to weed out those up front as they will be few.
So my take is to get all the opened ones and use simple .net iteration to remove the few that may be, leaving a nice and orderly and easily maintainable method.
public List<Notification> GetValidSubjectStatusList(IMongoCollection<Notification> mongoCollection){
var builder = Builders<Notification>.Filter;
var filter = builder.Eq(x => x.Events.FirstOrDefault().Status, "opened");
var listOf = mongoCollection.Find(filter).ToList();
var reducedList = new List<Notification>();
foreach(var hit in listOf){
if(hit.Events.Any()
&& hit.Events.First()
.Date.Equals(hit.Events
.OrderByDescending(x => x.Date)
.FirstOrDefault()
))
{
reducedList.Add(hit);
}
}
return reducedList;
}
I've created my MongoDB documents below with subdocuments/arrays, however the arrays aren't named and I would like to delete the whole subdocument if a match is found on an elements within a subdocument.
For example, if a match is found on userID and userLogs.Name. My query is deleting the whole documents instead of only the userLog array. I've also tried other methods such as Pull and PullFilter whilst researching this topic but it doesn't seem to work with this structure, please can you advise on whether there is a way or if I will have to change my document structure?
Document
{
"_id" : ObjectId("43535"),
"userID" : "1",
"userLogs" : [
{
"logID" : 1,
"Name" : "Book 1",
"Genre" : "Fiction",
....
},
{
"logID" : 2,
"Name" : "Book 2",
"Genre" : "Non-Fiction",
....
}
]
}
C# Code behind
var collection = db.GetCollection<BsonDocument>("Users");
var arrayFilter = Builders<BsonDocument>.Filter.Eq("userID", uID) &
Builders<BsonDocument>.Filter.Eq("userLogs.Name", name);
collection.DeleteOne(arrayFilter);
Thank you Christoph! I also solved this using the following method:
var query = Builders<BsonDocument>.Filter.Eq("UserID", uID) &
Builders<BsonDocument>.Filter.Eq("userLogs.Name", name);
var update = Builders<BsonDocument>.Update.Pull("userLogs", new BsonDocument(){
{ "Name", name }
});
collection.UpdateOne(query, update);
If I have a document with an array that contains arrays, how can I update a field of the second array?
For example, using the MongoDB C# driver, I want to update the field IWantToUpdateThis where the value is John Smith:
{
{
"_id" : 0,
"Guff" : "Blah",
"FirstArray" : [
{
"Blah" : "Guff",
"SecondArray" : [
{
"IWantToUpdateThis" : "John Smith",
"ButNotThis" : "Not me"
},
{
"IWantToUpdateThis" : "Will Smith",
"ButNotThis" : "Not me"
}
]
}
]
} }
I tried various options such as:
var filter = Builders<BsonDocument>.Filter.Eq("FirstArray.SecondArray.IWantToUpdateThis", "John Smith");
var update = Builders<BsonDocument>.Update.Set("FirstArray.SecondArray.$.IWantToUpdateThis", "My New Value");
var result = collection.UpdateOne(filter, update);
But I can't seem to update the value.
Edited to add:
The MongoDB version used when the question was posed was v3.2.12-69-g45cc6d2
I've figured out something that works, but I can't believe it is the best solution to the problem:
var collection = _database.GetCollection<Model>("Stuff");
var filter = Builders<Model>.Filter.Eq("FirstArray.SecondArray.IWantToUpdateThis", OldValue);
var docIds = collection.Find(filter).Project(x => x.Id).ToList();
foreach (var docId in docIds)
{
var model = collection.Find(e => e.Id == docId).FirstOrDefault();
foreach (var first in model.FirstArray)
{
foreach(var second in first.SecondArray)
{
if (second.IWantToUpdateThis == OldValue)
second.IWantToUpdateThis = NewValue;
}
}
collection.ReplaceOne(r => r.Id == docId, model);
}
If anyone comes up with another answer, it is very likely to be a better way than mine, and I leave this answer here only to show a possible way forward should anyone else find themselves in the same dead end as I.
I have a document like this
{
"_id": "63dafa72f21d48312d8ca405",
"tasks": [{
"_ref": "63d8d8d01beb0b606314e322",
"data": {
"values": [{
"key": "Deadline",
"value": "2014-10-13"
}]
}
}, {
"_ref": "84dd046c6695e32322d842f5",
"data": {
"values": []
}
}]
}
Now I want to update the value inside values which is inside data if the _ref field do match my input.
My code so far:
public bool updateProject(Project dbPro, Project pro)
{
var collection = db.GetCollection<BsonDocument>("projects");
var filter = Builders<BsonDocument>.Filter.Eq("_id", ObjectId.Parse( dbPro.Id));
var update = Builders<BsonDocument>.Update.AddToSetEach("tasks", pro.Tasks);
var result = collection.UpdateOne(filter, update);
if (result.IsModifiedCountAvailable)
{
if (result.ModifiedCount == 1)
{
return true;
}
}
return false;
}
At the moment this code does only append the documents as new tasks instead to append the values to the matching tasks. Maybe someone has an idea how to achieve this behavior?
UPDATE
I tried it like #Shane Oborn said. But its still not working for me.
var collection = db.GetCollection<BsonDocument>("projects");
var filter = Builders<BsonDocument>.Filter.Eq("_id", ObjectId.Parse( dbPro.Id));
var update = Builders<BsonDocument>.Update.Push("tags", buildBsonArrayFromTags(pro.Tags));
var result = collection.UpdateOne(filter, update);
if (result.IsModifiedCountAvailable)
{
if (result.ModifiedCount == 1)
{
return true;
}
}
return false;
}
Instead to override the data it appends an array to my array.
UPDATE
OK instead of push i did need set. And it worked then.
I don't have the exact code accessible, but close. I have a method that performs "upserts" (which "adds" if new, or "updates" if existing). This should get you close:
// The variable "doc" below is a BsonDocument
var updateRequests = new List<WriteModel<BsonDocument>>();
updateRequests.Add(new ReplaceOneModel<BsonDocument>(
CreateBsonDocumentFilterDefinition(filterKeyName, filterKeyValue), doc)
{
IsUpsert = true
});
var writeResult = await collection.BulkWriteAsync(updateRequests);
The key objects here for you are "ReplaceOneModel" and the "IsUpsert" property for the filter definition.
Good luck!
UPDATE:
Another method I have that does updates in subdocuments looks like this:
// Below, "subDocument" is a BsonDocument, and "subDocArrayName" is a string
// that should match the name of the array that contains your sub-document
// that will be updated.
var collection = _database.GetCollection<BsonDocument>(collectionName);
var builder = Builders<BsonDocument>.Update;
var update = builder.Push(subDocArrayName, subDocument);
await collection.UpdateOneAsync(CreateBsonDocumentFilterDefinition(filterKeyName, filterKeyValue), update);
Trying to get a query that has both AND and OR to work in C#.
In SQL Server would be something like:
... where (Code = 'abc' OR Description = 'def') AND (Flag = 0)
With MongoVUE I seemed to have gotten it to work with this:
{ "$or" : [{ "Description" : /def/i }, { "Code" : /abc/i }], "$and": [{ "Flag" : 0 }] }
but can't seem to get it with the C#:
tried this:
List<IMongoQuery> qryValue = new List<IMongoQuery>();
qryValue.Add(Query.EQ("Code", "abc"));
qryValue.Add(Query.EQ("Description", "def"));
qryValue.Add(Query.And(Query.EQ("Flag", 1)));
var query = Query.Or(qryValue.ToArray());
but get this back:
{ "$or" : [{ "Code" : "abc" }, { "Description" : "def" }, { "Flag" : 1 }] }
and this doesn't give the correct results: missing the AND part.
Anyone can help with this?
You're getting that back because you're are putting all of your Query.EQ into the Query.Or on the last line. Passing in a single value to a Query.And doesn't do anything; it's the same as if you passed a single value to any other method. You haven't and-ed it with anything.
You need to write it in code as you have above in SQL: respect the parenthesis.
Group your Or together first, and pass it as the first parameter to the And, followed by the single, additional clause for Flag.
var clauses = new[] { Query.EQ("Code", "abc"), Query.EQ("Description", "def") };
var query = Query.AND(Query.OR(clauses), Query.EQ("Flag", 1))
Something like that should work.