Mongodb updateMany property to exact value of other property - mongodb

I have documents representing Users with onboarding data stored as a nested object:
{
"_id" : ObjectId("5c7eb0132e6f793bcc7f4bf7"),
"userName" : "sample_user_name",
"onBoarding" : {
"completed" : ISODate("2019-03-05T17:46:28.803Z"),
"stepId" : 8,
"started" : null
}
}
But due to a bug we are missing a date when onboarding was started, I would like to "retrieve" this information by running an update operation where "started" will be set to the same date as "completed". I have a query like:
db.getCollection('user').updateMany(
{
$and: [
{"onBoarding.started": {$exists: false}},
{"onBoarding.completed": {$exists: true}}
]},
{
$set: { "onBoarding.started": "$onBoarding.completed" }
})
This however, sets "started" to "$onBoarding" literally (as a string).
{
"_id" : ObjectId("5c7eb0132e6f793bcc7f4bf7"),
"userName" : "sample_user_name",
"onBoarding" : {
"completed" : ISODate("2019-03-05T17:46:28.803Z"),
"stepId" : 8,
"started" : "$onBoarding"
}
}
How should I write it for mongo to take a value from "onBoarding.completed" and copy this value to "onBoarding.started"??
Expected result document should look like:
{
"_id" : ObjectId("5c7eb0132e6f793bcc7f4bf7"),
"userName" : "sample_user_name",
"onBoarding" : {
"completed" : ISODate("2019-03-05T17:46:28.803Z"),
"stepId" : 8,
"started" : ISODate("2019-03-05T17:46:28.803Z")
}
}

You need to use an aggregation pipeline to be able to use the value of another field :
db.user.updateMany(
{ <your query selector > },
[
{ $set: { onBoarding.started: "$onBoarding.completed" } },
]
)
Be aware that here, $set refers to the aggregation pipeline stage and not the update operator $set : https://docs.mongodb.com/manual/reference/method/db.collection.updateMany/index.html#update-with-aggregation-pipeline

The exists operator checks for existence of a field. If a field has a value of null, the query still returns true (because the field still exists, only its value is null).
The following queries behave differently with this input document: { _id: 1, fld1: 123, fld2: null }
db.test.find( { fld2: { exists: false } } ) returns false.
db.test.find( { fld2: null } } ) returns true.
Coming back to the data in question - the following query / script will update all the documents with following condition: ( "onBoarding.started" is null or the field doesn't exist ) and ( "onBoarding.completed" field exists and is not null ).
db.test.find( { $and: [ { $or: [ { "onBoarding.started": null }, { "onBoarding.started": { $exists: false } } ] }, { $and: [ { "onBoarding.completed": { $exists: true } }, { "onBoarding.completed": { $ne: null } } ] } ] } ).forEach( doc => db.test.updateOne( { _id: doc._id }, { $set: { "onBoarding.started" : doc.onBoarding.completed } } ) )
Using MongoDB version 4.2, the same update can be run as follows. Note the update uses an Aggregation stage (see documetation on update).
db.test.updateMany(
{ $and: [ { $or: [ { "onBoarding.started": null }, { "onBoarding.started": { $exists: false } } ] }, { $and: [ { "onBoarding.completed": { $exists: true } }, { "onBoarding.completed": { $ne: null } } ] } ] },
[
{ $addFields:
{ "onBoarding.started" : "$onBoarding.completed" }
}
]
)

Related

MongoDB query to replace NULL values

I'm currently working with a MongoDB database and I have fields that have a value of NULL is there a way to run a query that will replace these NULL fields with a value of "Missing" instead?
An example of the document is:
{
"_id" : 1,
"Users" : [
{
"name" : "John Davies",
"age" : null,
"place_of_birth" : "Cardigan"
},
{
"name" : "Edward Jones",
"age" : null,
"place_of_birth" : null
},
{
"name" : "Daniel Rhys",
"age" : NumberLong(63),
"place_of_birth" : "Cardigan"
},
{
"name" : null,
"age" : NumberLong(61),
"place_of_birth" : "Cardigan"
},
{
"name" : "John Davies ",
"age" : null,
"place_of_birth" : "Cardigan"
}
]
}
Demo - https://mongoplayground.net/p/dsI5G6zfbLr
Use $[]
db.collection.update(
{},
{ $set: { "Users.$[u].age": "missing" } },
{ arrayFilters: [ { "u.age": null } ], multi: true}
)
Combine multiple queries into 1 using db.collection.bulkWrite
db.collection.bulkWrite( [
{ updateMany :
{
"filter": {},
"update": { $set: { "Users.$[u].age": "missing" } },
"arrayFilters": [ { "u.age": null } ],
}
},
{ updateMany :
{
"filter": {},
"update": { $set: { "Users.$[u].name": "missing" } },
"arrayFilters": [ { "u.name": null } ],
}
},
{ updateMany :
{
"filter": {},
"update": { $set: { "Users.$[u].place_of_birth": "missing" } },
"arrayFilters": [ { "u.place_of_birth": null } ],
}
}
] )
Update for MongoDB Version 3.2+
while (db.collection.find({$or:[{"Users.age":null},{"Users.name":null},{"Users.place_of_birth":null}]}).count()) {
db.collection.bulkWrite( [
{ updateMany :
{
"filter": { "Users.age": null },
"update": { $set: { "Users.$.age": "missing" } }
}
},
{ updateMany :
{
"filter": { "Users.name": null },
"update": { $set: { "Users.$.name": "missing" } },
}
},
{ updateMany :
{
"filter": { "Users.place_of_birth": null },
"update": { $set: { "Users.$.place_of_birth": "missing" } },
}
}
] )
}
Try update with aggregation pipeline starting from MongoDB 4.2,
$map to iterate loop of Users array
$objectToArray to convert current object in $map to array key-value pair
$map to iterate loop of above converted array
$ifNull to check if value is null then replace Missing otherwise remain same
$arrayToObject convert above key-value array to object format
db.collection.update({},
[{
$set: {
Users: {
$map: {
input: "$Users",
in: {
$arrayToObject: {
$map: {
input: { $objectToArray: "$$this" },
in: {
k: "$$this.k",
v: { $ifNull: ["$$this.v", "Missing"] }
}
}
}
}
}
}
}
}],
{ multi: true }
)
Playground
MongoDB version 3.2 or above:
set default value for replacement in variable nullReplace
find() query to get all documents from your collection and loop through forEach
for loop of user object and check condition if value is null then replace nullReplace variable
return user oibject
updateOne() to update updated Users array in your collection
var nullReplace = "Missing";
db.collection.find({}).forEach(function(doc) {
var Users = doc.Users.map(function(u) {
for (var u in userObj) {
if (userObj[u] === null) userObj[u] = nullReplace;
}
return userObj;
})
db.collection.updateOne({ _id: doc._id }, { $set: { Users: Users } });
});

How to find documents that contains a field that contains any sub-fields

I have documents that have an attributes field. Something like this:
{
"_id" : "somestring",
...,
"attributes" : {
"SomeKey" : {
"code" : "SomeCode",
"name" : "SomeName",
}
}
}
How do I find all documents that have an attributes field that have 1 or more sub-fields?
The above document would match but the below document would not.
{
"_id" : "somestring",
...,
"attributes" : {}
}
I know how to query for arrays that have a number of items, and query for documents that have a field that has some specific sub-field, but I'm looking for a document that has a field that has any sub-fields.
Instead of using $where to run .js code thru query, you can use try as below :
db.collection.aggregate([
{
$match: {
attributes: {
$ne: {}
}
}
}
])
/** Or with `.find()` */
db.collection.find({ attributes: { $ne: {} } });
Test : MongoDB-Playground
Just in case if you don't have attributes at all or it exists but not an object then :
db.collection.aggregate([
{
$match: {
$expr: {
$and: [
{ $eq: [ { $type: "$attributes" } , "object" ] },
{ $ne: ["$attributes" , {} ] }
]
}
}
}
])
/** Or with `.find()` */
db.collection.find({
$expr: {
$and: [
{ $eq: [{ $type: "$attributes" }, "object"] },
{ $ne: ["$attributes", {}] },
],
},
});
Test : MongoDB-Playground
I found a mechanism that uses $where, but this can run slow because it's Javascript. None of the other built-in operators seem to fit.
db.getCollection('COL')
.find({
$where: function() {
for (field in this["attributes"])
return true;
return false;
}
})

MongoDB $cond with embedded document array

I am trying to generate a new collection with a field 'desc' having into account a condition in field in a documment array. To do so, I am using $cond statement
The origin collection example is the next one:
{
"_id" : ObjectId("5e8ef9a23e4f255bb41b9b40"),
"Brand" : {
"models" : [
{
"name" : "AA"
},
{
"name" : "BB"
}
]
}
}
{
"_id" : ObjectId("5e8ef9a83e4f255bb41b9b41"),
"Brand" : {
"models" : [
{
"name" : "AG"
},
{
"name" : "AA"
}
]
}
}
The query is the next:
db.runCommand({
aggregate: 'cars',
'pipeline': [
{
'$project': {
'desc': {
'$cond': {
if: {
$in: ['$Brand.models.name',['BB','TC','TS']]
},
then: 'Good',
else: 'Bad'
}
}
}
},
{
'$project': {
'desc': 1
}
},
{
$out: 'cars_stg'
}
],
'allowDiskUse': true,
})
The problem is that the $cond statement is always returning the "else" value. I also have tried $or statement with $eq or the $and with $ne, but is always returning "else".
What am I doing wrong, or how should I fix this?
Thanks
Since $Brand.models.name returns an array, we cannot use $in operator.
Instead, we can use $setIntersection which returns an array that contains the elements that appear in every input array
db.cars.aggregate([
{
"$project": {
"desc": {
"$cond": [
{
$gt: [
{
$size: {
$setIntersection: [
"$Brand.models.name",
[
"BB",
"TC",
"TS"
]
]
}
},
0
]
},
"Good",
"Bad"
]
}
}
},
{
"$project": {
"desc": 1
}
},
{
$out: 'cars_stg'
}
])
MongoPlayground | Alternative $reduce

How can I find all records where $type of field not equal 'date'?

I want get all records from collection, there deleted is not true, and $type of 'startDate' or 'endDate' is not 'date'. I am trying to use this find query:
{
'$and' :
{
{'deleted' : {'$ne' : true}},
{
'$or' :
{
{'startDate' : {'$type' : {'$ne' : 'date'}}},
{'endDate' : {'$type' : {'$ne' : 'date'}}},
}
}
}
}
But I get error: "argument to $type is not a number or a string". How can I get desired result?
Your $type and $ne are the wrong way round. Try something like this:
{
$and: [
{ deleted: { $ne: true } },
{
$or: [
{ startDate: { $not: { $type: "date" } } },
{ endDate: { $not: { $type: "date" } } }
]
}
];
}
I've changed the order of $type and $ne and changed $ne to $not. I've also swapped out the objects for arrays in your $and and $or queries.

How to get values in mongdb

This is my my data in Mongodb
{
"d" : {
"results" : [
{
"slack_id" : "RAGHU#TN.COM",
"connector_id" : "GRECLNT900",
"sys_role" : "DEV",
"user_id" : "RAGHU"
},
{
"slack_id" : "RAGHU#TN.COM",
"connector_id" : "GRECLNT900",
"sys_role" : "PRD",
"user_id" : "RAGHU",
"question" : "What is your favorite color?",
"Answer" : "Orange"
},
]
}
}
If i am giving RAGHU#TN.COM. then i want display sys_role. Output like this[DEV, PRD]
I am trying this way
x = mydb.mycollection.distinct("sys-role")
But I get an empty array like [ ]
You have to treat the cursor as a reference(personally I see it as a reference in C), and then de-reference it to see the result.(What is inside the address)
For the specific column, here is the result from command prompt:
my_cursor = mydb.mycollection.distinct("sys-role")
for x in my_cursor:
print('{0}'.format(x['sys_role']))
The distinct operator is not inter-operatable thus it's hard to filter by slack_id first. I would recommande using aggregation pipelines.
Here is an example.
[
{
'$match': {
'slack_id': 'RAGHU#TN.COM'
}
}, {
'$group': {
'_id': 'slack_id',
'result': {
'$addToSet': 'sys_role'
}
}
}
]
With this pipeline, your sys_role set will be in the .result field.
Using Mongo aggregation query you will get required result set. Try this:
db.collection.aggregate([
{
"$match": {
"d.results.slack_id": "RAGHU#TN.COM"
}
},
{
$group: {
_id: "$d.results.slack_id",
sys_role: {
$push: "$d.results.sys_role"
}
}
}
])
db.getCollection("collection").aggregate(
// Pipeline
[
// Stage 1
{
$project: {
results: {
$filter: {
input: "$d.results",
as: "item",
cond: { $eq: [ "$$item.slack_id", 'RAGHU#TN.COM' ] }
}
}
}
},
// Stage 2
{
$unwind: {
path : "$results",
preserveNullAndEmptyArrays : false // optional
}
},
// Stage 3
{
$group: {
_id:'$results.slack_id',
sys_roles:{$addToSet:'$results.sys_role'}
}
},
]
);