Update an array value in nested document in ArangoDB - arangodb

`Given the following document inside a collection card: I have to update the whole data value for a particular id in staticCard
`
{
"staticCards": [
{
id:123,
search:"",
data:[]
},
{
id:456,
search:"",
data:[]
},
],
"dynamicCards":[
{
id:789,
search:"",
data:[]
},
{
id:127,
search:"",
data:[]
},
{}
]
}

You need to determine the index of the array element, which isn't straightforward if you want to match one of the object attributes instead of the whole object. POSITION() is not an option in this case, but you can solve it with a subquery. Then you can use REPLACE_NTH() to set a new value. Finally, you need to update the respective top-level attribute.
LET pos = FIRST(FOR i IN 0..LENGTH(doc.dynamicCards)-1
FILTER doc.dynamicCards[i].id == 127
LIMIT 1
RETURN i
)
LET new = REPLACE_NTH(doc.dynamicCards, pos, { id: 128, search: "", data: [] })
UPDATE doc WITH { dynamicCards: new } IN coll

Related

Update collection to change the rank

i have a mongodb collection that I sort by the amount of points each item has, and it shows a rank according to it's place in the collection :
db.collection('websites').find({}).sort({ "points": -1 }).forEach(doc => {
rank++;
doc.rank = rank;
delete doc._id;
console.log(doc)
Si I thought to myself : Ok, I'm gonna update the rank in the collection, so I added this :
db.collection('websites').updateMany({},
{ $set: { rank: doc.rank } }
)
But I was too good to be true, and it updates every single item with the same rank, which changes at each refresh, what exactly is going on, here ?
EDIT : I managed to do it by doing this :
rank = 0;
db.collection('websites').find({}).sort({ "points": -1 }).forEach(doc => {
rank++;
doc.rank = rank;
//delete doc._id;
console.log(doc._id);
db.collection('websites').updateMany({_id : doc._id},
{ $set: { rank: doc.rank } },
{ upsert: true }
)
})
Try this:
db.collection('websites')
.updateOne( //update only one
{rank: doc.rank}, //update the one where rank is the sent in parameter doc.rank
{ $set: { rank: doc.rank } } // if multiple docs have the same rank you should send in more parameters
)
db.collection('websites').updateMany({/*All docs match*/},
{ $set: { rank: doc.rank } }
)
Reason it updates same rank because you have no filter which means it matches all docs in the collection and you have updateMany
You need to set a filter to restrict docs to be updated.
db.collection('websites').updateMany({id: "someID"},
{ $set: { rank: doc.rank } }
)
The OP states we want to sort all the docs by points, then "rerank" them from 1 to n in that order and update the DB. Here is an example of where "aggregate is the new update" thanks to the power of $merge onto the same collection as the input:
db.foo.aggregate([
// Get everything in descending order...
{$sort: {'points':-1}}
// ... and turn it into a big array:
,{$group: {_id:null, X:{$push: '$$ROOT'}}}
// Walk the array and incrementally set rank. The input arg
// is $X and we set $X so we are overwriting the old X:
,{$addFields: {X: {$function: {
body: function(items) {
for(var i = 0; i < items.length; i++) {
items[i]['rank'] = (i+1);
}
return items;
},
args: [ '$X' ],
lang: "js"
}}
}}
// Get us back to regular docs, not an array:
,{$unwind: '$X'}
,{$replaceRoot: {newRoot: '$X'}}
// ... and update everything:
,{$merge: {
into: "foo",
on: [ "_id" ],
whenMatched: "merge",
whenNotMatched: "fail"
}}
]);
If using $function spooks you, you can use a somewhat more obtuse approach with $reduce as a stateful for loop substitute. To better understand what is happening, block comment with /* */ the stages below $group and one by one uncomment each successive stage to see how that operator is affecting the pipeline.
db.foo.aggregate([
// Get everything in descending order...
{$sort: {'points':-1}}
// ... and turn it into a big array:
,{$group: {_id:null, X:{$push: '$$ROOT'}}}
// Use $reduce as a for loop with state.
,{$addFields: {X: {$reduce: {
input: '$X',
// The value (stateful) part of the loop will contain a
// counter n and the array newX which we will rebuild with
// the incremental rank:
initialValue: {
n:0,
newX:[]
},
in: {$let: {
vars: {qq:{$add:['$$value.n',1]}}, // n = n + 1
in: {
n: '$$qq',
newX: {$concatArrays: [
'$$value.newX',
// A little weird but this means "take the
// current item in the array ($$this) and
// set $$this.rank = $qq by merging it into the
// item. This results in a new object but
// $concatArrays needs an array so wrap it
// with [ ]":
[ {$mergeObjects: ['$$this',{rank:'$$qq'}]} ]
]}
}
}}
}}
}}
,{$unwind: '$X.newX'}
,{$replaceRoot: {newRoot: '$X.newX'}}
,{$merge: {
into: "foo",
on: [ "_id" ],
whenMatched: "merge",
whenNotMatched: "fail"
}}
]);
The problem here is that mongo is using the same doc.rank value to update all the records that match the filter criteria (all records in your case). Now you have two options to resolve the issue -
Works but is less efficient) - Idea here is that you need to calculate the rank for each website that you want to update. loop throuh all the document and run below query which will update every document with it's calculated rank. You could probably think that this is inefficient and you would be right. We are making large number of network calls to update the records. Worse part is that the slowness is unbounded and will get slower as number of records increases.
db.collection('websites')
.updateOne(
{ id: 'docIdThatNeedsToBeUpdated'},
{ $set: { rank: 'calculatedRankOfTheWebsite' } }
)
Efficient option - Use the same technique to calculate the rank for each website and loop through it to generate the update statement as above. But this time you would not make the update calls separately for all the websites. Rather you would use Bulk update technique. You add all your update statement to a batch and execute them all at one go.
//loop and use below two line to add the statements to a batch.
var bulk = db.websites.initializeUnorderedBulkOp();
bulk.find({ id: 'docIdThatNeedsToBeUpdated' })
.updateOne({
$set: {
rank: 'calculatedRankOfTheWebsite'
}
});
//execute all of the statement at one go outside of the loop
bulk.execute();
I managed to do it by doing:
rank = 0;
db.collection('websites').find({}).sort({ "points": -1 }).forEach(doc => {
rank++;
doc.rank = rank;
//delete doc._id;
console.log(doc._id);
db.collection('websites').updateMany({_id : doc._id},
{ $set: { rank: doc.rank } },
{ upsert: true }
)
})
Thank you everyone !

Mondodb update array inside array (embeded) (mongoose/nodejs)

Hello i'm trying to update an embeded document inside an embeded document.
My "object" looks something like this
{
_id:0,
fieldOne:"f1",
fieldTwo : "f2",
subDocument:[
{
_id:0,
subfieldOne:"f1",
subfieldTwo:"f2",
subSubDocument:[
{
_id:0,
sub_subfieldOne:"f1",
sub_subfieldTwo:"f2"
}
]
},
],
}
afer some resarch i found out that you can update a sub sub array item if you know his position,
something like this
await Document.updateOne(
{ "subDocument._id": 0},
{ $set:{"subDocument.0.subsubDocument.0.sub_subfieldOne":"testaroo"} });
howerver if i loop through the arrays like
for(let i = 0;i<subDocument.length;i++){
for(let j = 0;j<subDocument[i].subSubDocument.length;j++){
await Document.updateOne(
{ "subDocument._id": 0},
{ $set:{"subDocument."+i+".subsubDocument."+j+".sub_subfieldOne":"testaroo"} });
}
}
he does not like the string concatenation in the query like this.....is there a way to do it?
After reading your code, I think you are trying to update sub_subfieldOne of all sub documents. If so, you can use $[] operator. It indicates that the update operator should modify all elements in the specified array field:
await Document.updateOne(
{ "subDocument._id": 0},
{ $set:{"subDocument.$[].subsubDocument.$[].sub_subfieldOne": "testaroo"} }
);

Updating mongoose nested array of mixed types

I have a mongoose schema of mixed types like so:
const user = mongoose.Schema({
...
"links" : []
...
After populating this schema, I ended up with data like so:
[
[
{
"step1": "post-url-google", // This field is unique for each entry
"step2": {
"title": "Heading 1",
"likes": 4
}
},
],
[
{
"step1": "post-url-microsoft",
"step2": {
"title": "Heading 1",
"likes": 1
}
},
{
"step1": "post-url-apple",
"step2": {
"title": "Heading 2",
"likes": 6 // I want to update this to 7
}
}
]
]
What I want to achieve is to update the "step1": "post-url-apple" field from 6 to have a likes of 7
So I tried using the User.save() function like so:
let user = await User.findOne({"_id" : "some_id"})
user.links[1].some(object => {
if (object.step1 === "post-url-apple") {
object.step2.likes = 7
(async function (){
user.save() // I also did error handling
})()
return
}
})
This method works fine and the user gets updated but it keeps throwing ParallelSaveError possibly because I am calling the save() function in parallel on the same user instance in some other parts of my code.
So I decided to use the User.findOneAndUpdate() method, but my queries keep failing when using the mongodb dot notation $[<identifier>], obviously because I don't know how to use it properly.
Like so:
let update = {
"$set" : {
"links.$[index1].$[index2].step2.likes" : 7,
}
}
let conditions = {
arrayFilters : [
{"index1" : 1},
{"index2.step1" : "post-url-apple"}
]
}
try {
let result = await Users.findOneAndUpdate({"_id" : "some_id"}, update, conditions)
console.log(result)
} catch (err) {
console.log(err)
}
For all good reasons, I'm not hiting the catch block but the update was equally not successful
How do I achieve updating the "step1": "post-url-apple" likes field to 7 using findOneAndUpdate?
Thank you.
In arrayFilters you should define the conditions to be applied to all the array elements, not the index
If you are sure, you always update the second array element (index = 1) of the outer array, then you can use the dot notation for the outer array, and for the inner array you can use the array filters to get the element that has step1 = 'post-url-apple'
your code may look something like that
let update = {
"$set" : {
'links.1.$[item].step2.likes': 7 // here we used links.1 to access the second element of the outer array
}
}
let conditions = {
arrayFilters : [
{ 'item.step1' : 'post-url-apple' } // item here is the element in the inner array that has step1 = post-url-apple
]
}
then do your update query
hope it helps

How to pull a range of objects from an array of objects and re-insert them at a new position in the array?

Desired Behaviour
Pull a range of objects from an array of objects and push them back to the array at a new index.
For example, pull objects from the array where their index is between 0 and 2, and push them back to the array at position 6.
For reference, in jQuery, the desired behaviour can be achieved with:
if (before_or_after === "before") {
$("li").eq(new_position).before($("li").slice(range_start, range_end + 1));
} else if (before_or_after === "after") {
$("li").eq(new_position).after($("li").slice(range_start, range_end + 1));
}
jsFiddle demonstration
Schema
{
"_id": ObjectId("*********"),
"title": "title text",
"description": "description text",
"statements": [
{
"text": "string",
"id": "********"
},
{
"text": "string",
"id": "********"
},
{
"text": "string",
"id": "********"
},
{
"text": "string",
"id": "********"
},
{
"text": "string",
"id": "********"
}]
}
What I've Tried
I am able to reposition a single object in an array of objects with the code below.
It uses pull to remove the object from the array and push to add it back to the array at a new position.
In order to do the same for a range of objects, I think I just need to modify the $pull and $push variables but:
I can't figure out how to use $slice in this context, either as a projection or an aggregation, in a $pull operation
Because I can't figure out the first bit, I don't know how to attempt the second bit - the $push operation
// define the topic_id to search for
var topic_id = request_body.topic_id;
// make it usable as a search query
var o_id = new ObjectID(topic_id);
// define the statement_id to search for
var statement_id = request_body.statement_id;
// define new position
var new_position = Number(request_body.new_position);
// define old position
var old_position = Number(request_body.old_position);
// define before or after (this will be relevant later)
// var before_or_after = request_body.before_or_after;
// define the filter
var filter = { _id: o_id };
// define the pull update - to remove the object from the array of objects
var pull_update = {
$pull: {
statements: { id: statement_id } // <----- how do i pull a range of objects here
}
};
// define the projection so that only the 'statements' array is returned
var options = { projection: { statements: 1 } };
try {
// perform the pull update
var topic = await collection.findOneAndUpdate(filter, pull_update, options);
// get the returned statement object so that it can be inserted at the desired index
var returned_statement = topic.value.statements[old_position];
// define the push update - to add the object back to the array at the desired position
var push_update = {
$push: {
statements: {
$each: [returned_statement],
$position: new_position
}
} // <----- how do i push the range of objects back into the array here
};
// perform the push update
var topic = await collection.findOneAndUpdate(filter, push_update);
}
Environments
##### local
$ mongod --version
db version v4.0.3
$ npm view mongodb version
3.5.9
$ node -v
v10.16.3
$ systeminfo
OS Name: Microsoft Windows 10 Home
OS Version: 10.0.18363 N/A Build 18363
##### production
$ mongod --version
db version v3.6.3
$ npm view mongodb version
3.5.9
$ node -v
v8.11.4
RedHat OpenShift Online, Linux
Edit
Gradually, figuring out parts of the problem, I think:
Using the example here, the following returns objects from array with index 0 - 2 (ie 3 objects):
db.topics.aggregate([
{ "$match": { "_id": ObjectId("********") } },
{ "$project": { "statements": { "$slice": ["$statements", 0, 3] }, _id: 0 } }
])
Not sure how to use that in a pull yet...
I also looked into using $in (even though i would prefer to just grab a range of objects than have to specify each object's id), but realised it does not preserve the order of the array values provided in the results returned:
Does MongoDB's $in clause guarantee order
Here is one solution to re-ordering results from $in in Node:
https://stackoverflow.com/a/34751295
Here an example with mongo 3.5
const mongo = require('mongodb')
;(async function (params) {
const client = await mongo.connect('mongodb://localhost:27017')
const coll = client.db('test').collection('test')
const from0to99 = Array(100).fill('0').map((_, i) => String(i))
const from5To28 = Array(24).fill('0').map((_, i) => String(i + 5))
const insert = { statements: from0to99.map(_ => ({ id: _ })) }
await coll.insertOne(insert)
const all100ElementsRead = await coll.findOneAndUpdate(
{ _id: insert._id },
{
$pull: {
statements: {
id: { $in: from5To28 }
}
}
},
{ returnOriginal: true }
)
/**
* It shows the object with the desired _id BEFORE doing the $pull
* You can process all the old elements as you wish
*/
console.log(all100ElementsRead.value.statements)
// I use the object read from the database to push back
// since I know the $in condition, I must filter the array returned
const pushBack = all100ElementsRead.value.statements.filter(_ => from5To28.includes(_.id))
// push back the 5-28 range at position 72
const pushed = await coll.findOneAndUpdate(
{ _id: insert._id },
{
$push: {
statements: {
$each: pushBack,
$position: 72 // 0-indexed
}
}
},
{ returnOriginal: false }
)
console.log(pushed.value.statements) // show all the 100 elements
client.close()
})()
This old issue helped
if you want "desired behavior" when mutating arrays ,
you add these to checklist:
array.length atleast==7 if you want to add ,splice at 6
creates a new array if u use concat
mutates orignal if used array.push or splice or a[a.length]='apple'
USE slice() to select between incex1 to index2.
or run a native for loop to select few elements of array or
apply a array.filter() finction.
once you select your elements which needed to be manupulated you mentioned you want to add it to end. so this is the method below.
about adding elements at end:
CONCAT EXAMPLE
const original = ['🦊']; //const does not mean its immutable just that it cant be reassigned
let newArray;
newArray = original.concat('🦄');
newArray = [...original, '🦄'];
// Result
newArray; // ['🦊', '🦄']
original; // ['🦊']
SPLICE EXAMPLE:
const zoo = ['🦊', '🐮'];
zoo.splice(
zoo.length, // We want add at the END of our array
0, // We do NOT want to remove any item
'🐧', '🐦', '🐤', // These are the items we want to add
);
console.log(zoo); // ['🦊', '🐮', '🐧', '🐦', '🐤']

Conditional filtering against a nested object in MongoDB

I am having a problem searching for a key of a nested object.
I have search criteria object that may or may not have certain fields I'd like to search on.
The way I'm solving this is to use conditional statements to append to a "match criteria" object that gets passed to the aggregate $match operator. it works well until I need to match to something inside a nested object.
Here is a sample document structure
{
name: string,
dates: {
actived: Date,
suspended: Date
},
address : [{
street: string,
city: string,
state: string,
zip: string
}]
};
My criteria object is populated thru a UI and passed a JSON that looks similar to this:
{
"name": "",
"state": ""
}
And although I can explicitly use "dates.suspended" without issue -
when I try to append address.state to my search match criteria - I get an error.
module.exports.search = function( criteria, callback )
let matchCriteria = {
"name": criteria.name,
"dates.suspended": null
};
if ( criteria.state !== '' ) {
// *** PROBLEM HAPPENS HERE *** //
matchCriteria.address.state = criteria.state;
}
User.aggregate([
{ "$match": matchCriteria },
{ "$addFields": {...} },
{ "$project": {...} }
], callback );
}
I get the error:
TypeError: Cannot set property 'state' of undefined
I understand that I'm specifying 'address.state' when 'address' doesn't exist yet - but I am unclear what my syntax would be surely it woulnd't be matchCriteria['address.state'] or "matchCriteria.address.state"
Is there a better way to do conditional filtering?
For search in Nested Object, You have to use unwind
A query that help you :
//For testing declare criteria as const
let criteria = {name : 'name', 'state' : 'state'};
let addressMatch = {};
let matchCriteria = {
"name": criteria.name,
"dates.suspended": null
};
if ( criteria.state) {
addressMatch = { 'address.state' : criteria.state };
}
db.getCollection('user').aggregate([{
$match :matchCriteria,
},{$unwind:'$address'},
{$match : addressMatch}
])
Firstly check for address, and then access the property as shown:
if(matchCriteria['address']) {
matchCriteria['address']['state'] = criteria['state'];
}
else {
//otherwise
}
This should fix it:
matchCriteria['address.state'] = criteria.state;

Resources