MongoDB Update array element (document with a key) if exists, else push - mongodb

I have such a schema:
doc:
{
//Some fields
visits:
[
{
userID: Int32
time: Int64
}
]
}
I want to first check if a specific userID exists, if not, push a document with that userID and system time, else just update time value. I know neither $push nor $addToSet are not able to do that. Also using $ with upsert:true doesn't work, because of official documentation advice which says DB will use $ as field name instead of operator when trying to upsert.
Please guide me about this. Thanks

You can use $addToSet to add an item to the array and $set to update an existing item in this array.
The following will add a new item to the array if the userID is not found in the array :
db.doc.update({
visits: {
"$not": {
"$elemMatch": {
"userID": 4
}
}
}
}, {
$addToSet: {
visits: {
"userID": 4,
"time": 1482607614
}
}
}, { multi: true });
The following will update the subdocument array item if it matches the userId :
db.doc.update({ "visits.userID": 2 }, {
$set: {
"visits.$.time": 1482607614
}
}, { multi: true });

const p = await Transaction.findOneAndUpdate(
{
_id: data.id,
'products.id': { $nin: [product.id] },
},
{
$inc: {
actualCost: product.mrp,
},
$push: {
products: { id: product.id },
},
},
{ new: true }
);
or
db.collection.aggregate([
{
"$match": {
"_id": 1
}
},
{
"$match": {
"sizes.id": {
"$nin": [
7
]
}
}
},
{
"$set": {
"price": 20
}
}
])
https://mongoplayground.net/p/BguFa6E9Tra

I know it's very late. But it may help others. Starting from mongo4.4, we can use $function to use a custom function to implement our own logic. Also, we can use the bulk operation to achieve this output.
Assuming the existing data is as below
{
"_id" : ObjectId("62de4e31daa9b8acd56656ba"),
"entrance" : "Entrance1",
"visits" : [
{
"userId" : 1,
"time" : 1658736074
},
{
"userId" : 2,
"time" : 1658736671
}
]
}
Solution 1: using custom function
db.visitors.updateMany(
{_id: ObjectId('62de4e31daa9b8acd56656ba')},
[
{
$set: {
visits: {
$function: {
lang: "js",
args: ["$visits"],
body: function(visits) {
let v = []
let input = {userId: 3, time: Math.floor(Date.now() / 1000)};
if(Array.isArray(visits)) {
v = visits.filter(x => x.userId != input.userId)
}
v.push(input)
return v;
}
}
}
}
}
]
)
In NodeJS, the function body should be enclosed with ` character
...
lang: 'js',
args: ["$visits"],
body: `function(visits) {
let v = []
let input = {userId: 3, time: Math.floor(Date.now() / 1000)};
if(Array.isArray(visits)) {
v = visits.filter(x => x.userId != input.userId)
}
v.push(input)
return v;
}`
...
Solution 2: Using bulk operation:
Please note that the time here will be in the ISODate
var bulkOp = db.visitors.initializeOrderedBulkOp()
bulkOp.find({ _id: ObjectId('62de4e31daa9b8acd56656ba') }).updateOne({$pull: { visits: { userId: 2 }} });
bulkOp.find({ _id: ObjectId('62de4e31daa9b8acd56656ba') }).updateOne({$push: {visits: {userId: 2, time: new Date()}}})
bulkOp.execute()
Reference link

Related

MongoDB search by first attr with value

Is it possible do same filtering as in js
const list = [
{
a: 1,
"mostImportant": "qwer",
"lessImportant": "rty"
},
{
a: 2,
"lessImportant": "weRt",
"notImportant": "asd",
},
{
a: 3,
"mostImportant": "qwe2",
"notImportant": "asd",
}
];
list.filter((data) => {
data.attrToSearch = data.mostImportant || data.lessImportant || data.notImportant;
return data.attrToSearch.match(/wer/i);
});
in MongoDB?
Loot at example:
https://mongoplayground.net/p/VQdfoQ-HQV4
So I want to attrToSearch contain value of first not blank attr with next order mostImportant, lessImportant, notImportant
and then match by regex.
Expected result is receive first two documents
Appreciate your help
Approach 1: With $ifNull
Updated
$ifNull only checks whether the value is null but does not cover checking for the empty string.
Hence, according to the attached JS function which skips for null, undefined, empty string value and takes the following value, you need to set the field value as null if it is found out with an empty string via $cond.
db.collection.aggregate([
{
$addFields: {
mostImportant: {
$cond: {
if: {
$eq: [
"$mostImportant",
""
]
},
then: null,
else: "$mostImportant"
}
},
lessImportant: {
$cond: {
if: {
$eq: [
"$lessImportant",
""
]
},
then: null,
else: "$lessImportant"
}
},
notImportant: {
$cond: {
if: {
$eq: [
"$notImportant",
""
]
},
then: null,
else: "$notImportant"
}
}
}
},
{
"$addFields": {
"attrToSearch": {
$ifNull: [
"$mostImportant",
"$lessImportant",
"$notImportant"
]
}
}
},
{
"$match": {
attrToSearch: {
$regex: "wer",
$options: "i"
}
}
}
])
Demo Approach 1 # Mongo Playground
Approach 2: With $function
Via $function, it allows you to write a user-defined function (UDF) with JavaScript support.
db.collection.aggregate([
{
"$addFields": {
"attrToSearch": {
$function: {
body: "function(mostImportant, lessImportant, notImportant) { return mostImportant || lessImportant || notImportant; }",
args: [
"$mostImportant",
"$lessImportant",
"$notImportant"
],
lang: "js"
}
}
}
},
{
"$match": {
attrToSearch: {
$regex: "wer",
$options: "i"
}
}
}
])
Demo Approach 2 # Mongo Playground

variable undefined after findOne operation mongodb

I am trying to make an API that makes use of 2 databases to generate a fine. Here is the code:
router.get("/generateFine/:bookingID/:currDate", function (req, res, next) {
var currDate,
returnDate,
fine,
totalFine = 0;
Booking.findOne({ _id: req.params.bookingID }).then(function (booking) {
Car.findOne({ _id: booking.carID }).then(function (car) {
currDate = Date.parse(req.params.currDate) / 1000 / 3600 / 24;
returnDate = Date.parse(booking.bookingDates[1]) / 1000 / 3600 / 24;
fine = car.fine;
if (currDate > returnDate) {
totalFine = fine * (currDate - returnDate);
}
console.log(totalFine);
// res.send(totalFine);
});
console.log("totalFine is " + totalFine);
// res.send(totalFine);
});
});
Here are the two Schemas used in the code:
Booking Schema:
{
"_id" : ObjectId("621bf46602edf12942f0d5c9"),
"carID" : "621b87af70c150da70b1dabf",
"bookingDates" : [
"2022-03-05",
"2022-03-06"
],
}
Car Schema:
{
"_id" : ObjectId("621b87af70c150da70b1dabf"),
"name" : "Toyota",
"rate" : 60,
"fine" : 10,
"datesBooked" : [
{
"from" : "2022-03-05",
"to" : "2022-03-06"
},
{
"from" : "2022-03-07",
"to" : "2022-03-08"
},
{
"from" : "2022-03-09",
"to" : "2022-03-10"
}
],
"__v" : 0
}
I want to return the generated fine to the user. When I am trying to send the result, it throwing an error. The first console log prints the correct result, but the second console log prints 0. Also, how can I send the result without getting an error.
Thanks already!
You could use $lookup aggregation pipeline stage to include the car document that matches on the carID field, create additional computed fields that will aid you in getting the total fine whilst using the necessary aggregation operators.
Essentially you would need to run an aggregate pipeline that follows:
const mongoose = require('mongoose');
router.get('/generateFine/:bookingID/:currDate', async function (req, res, next) {
const currDate = new Date(req.params.currDate);
const [{ totalFine }] = await Booking.aggregate([
{ $match: { _id: mongoose.Types.ObjectId(req.params.bookingID) }},
{ $lookup: {
from: 'cars', // or from: Car.collection.name
let: { carId: { $toObjectId: '$carID' } }, // convert the carID string field to ObjectId for the match to work correctly
pipeline: [
{ $match: {
$expr: { $eq: [ '$_id', '$$carId' ] }
} }
],
as: 'car'
} },
{ $addFields: {
car: { $arrayElemAt: ['$car', 0 ] }, // get the car document from the array returned above
returnDate: {
$toDate: { $arrayElemAt: ['$bookingDates', 1 ]}
}
} },
// compute the overdue days
{ $addFields: {
overdueDays: {
$trunc: {
$ceil: {
$abs: {
$sum: {
$divide: [
{ $subtract: [currDate, '$returnDate'] },
60 * 1000 * 60 * 24
]
}
}
}
}
}
} },
{ $project: { // project a new field
totalFine: {
$cond: [
{ $gt: [currDate, '$returnDate'] }, // IF current date is greater than return date
{ $multiply: ['$car.fine', '$overdueDays'] }, // THEN multiply car fine with the overdue days
0 // ELSE total fine is 0
]
}
} }
]).exec();
console.log("totalFine is " + totalFine);
// res.send(totalFine);
});

Mongo result set with multiple memberIds

I am using Meteor/Mongo with Typescript/Javascript.
I have a chat app I am developing. I get a result set from Mongo.
const chats: Mongo.Cursor<Chat> = Chats.find(
{ memberIds: 'J65'},
{
sort: { lastMessageCreatedAt: -1 },
transform: this.transformChat.bind(this),
fields: { memberIds: 1, lastMessageCreatedAt: 1 }
}
);
This returns the Chats for J65 .
Question
Is it possible to have multiple memberIds? i.e. return the results for more than one matching id
More info
I have tried this with no success, i.e. it returns nothing even though there is a match (no errors though).
let registeredIds: String[] = ['J65', 'J66'];
...
{ memberIds: registeredIds },
this is what is in the database:
[
{
"_id": "CHb8FSuGSfZMPhkrW",
"memberIds": [
"P9",
"J64"
]
},
{
"_id": "uMSJjHheTp7RhGdH3",
"memberIds": [
"P9",
"J65"
]
},
{
"_id": "e6ZMmRaJLPptF63z8",
"memberIds": [
"P9",
"J66"
]
}
]
You should use the $in operator.
Your code shall look like:
const chats: Mongo.Cursor<Chat> = Chats.find(
{ memberIds: {$in:['J65','J66','J67']},
{
sort: { lastMessageCreatedAt: -1 },
transform: this.transformChat.bind(this),
fields: { memberIds: 1, lastMessageCreatedAt: 1 }
}
);
Refer docs for more information.

wrong result in MongoDB mapreduce function?

I have Collection "cars" from that want to get count of certified cars as trueCount and flaseCount where certified is boolean.
am issuing the following mapreduce query
map:-
function() { for (var idx = 0; idx < this.cars.length; idx++) {
var key = this.cars[idx].carName;
var value = {
count : 1,
certifiedCheck : this.cars[idx].certified
};
emit(key, value);
} }
reduce:-
function(key, values) {
certifiedCount = { trueCount: 0, falseCount: 0 };
values.forEach(function(value) {
if ( value.certifiedCheck )
certifiedCount.trueCount += value.count;
else
certifiedCount.falseCount += value.count;
});
return certifiedCount;
query:
{ "type": "cars" }
getting the following result :
{ "id" : "carName" , "value" : { "true" : 277.0 , "false" : NaN}};
even though I have 457 documents in the collection.
Please someone help me here to fix this issue.
Thanks in advance
You mixed up your map-reduce: to reduce to two keys "true" and "false" you need to emit these as keys. Then, the reducer will run per key.
As pseudo code:
map:
for each car
evaluate whether it should be true or false
key = (true/false)
emit(key, { count : 1 })
reduce:
(input is true/false as key, array of count-documents as value)
for each value-document
sum up the count
return key, sum
This should yields two documents with true / false as key and the respective sum as value.
You should consider using the aggregation framework for running the aggregation since it achieves the same result albeit faster than MapReduce as aggregation runs natively in the server (C++), MapReduce spawns separate javascript thread(s) to run JS code.
Thus said, if you run the following aggregation pipeline which uses the $cond operator to evaluate the counts based on the logic in the field expression, you will get a similar result:
Because you haven't showed your collection schema, I've assumed the following sample documents with a cars field as array having seen in your mapReduce you are doing a for loop on the cars property:
Populate test collection
db.collection.insert([
{ _id: 1, cars: [ { model: "A", certified: true }, { model: "B", certified: true } ] },
{ _id: 2, cars: [ { model: "A", certified: false }, { model: "B", certified: true } ] },
{ _id: 3, cars: [ { model: "A", certified: true }, { model: "B", certified: false } ] },
{ _id: 4, cars: [ { model: "A", certified: true }, { model: "B", certified: false } ] },
{ _id: 5, cars: [ { model: "A", certified: true }, { model: "B", certified: true } ] }
])
Run aggregation operation
db.collection.aggregate([
{ "$unwind": "$cars" },
{
"$group": {
"_id": "$cars.model",
"trueCount": {
"$sum": {
"$cond": [ "$cars.certified", 1, 0 ]
}
},
"falseCount": {
"$sum": {
"$cond": [ "$cars.certified", 0, 1 ]
}
}
}
}
])
Result:
/* 1 */
{
"_id" : "A",
"trueCount" : 4,
"falseCount" : 1
}
/* 2 */
{
"_id" : "B",
"trueCount" : 3,
"falseCount" : 2
}

How to insert to the certain position in the array in the subdocument by using Mongoose an MongoDB 2.6

A great explanation of how to use new $position operator of Mongodb 2.6 by using Mongoose was given in the answer to my question. The suggested solution works perfect for simple arrays. If array is in subdocument or each element of array is array the suggested solution doesn't work. I mean something like this:
List.collection.update(
{/*....*/},
{ "$push": {
"subdocument.list": {
"$each": [ 1, 2, 3 ],
"$position": 0 }
}
},function(err,NumAffected) {
console.log("done");
});
List.collection.update(
{/*....*/},
{ "$push": {
"list1.$.list2": {
"$each": [ 1, 2, 3 ],
"$position": 0 }
}
},function(err,NumAffected) {
console.log("done");
});
In the current Mongoose version (4.4.2) you can use position like this:
Foo.update(query.id,
{
$push: {
arrayData: {
$each: [{
date: new Date
}], $position: 0
}
}
});
No sure what the issue is here:
db.list.insert({ "list": { "sub": [4] } })
db.list.update(
{},
{ "$push": { "list.sub": { "$each": [1,2,3], "$position": 0 } } }
)
{ "list" : { "sub" : [ 1, 2, 3, 4 ] } }
So that works as expected.
And for the other example:
db.list.insert({
outer: [
{
key: "a",
inner: [4]
},
{
key: "b",
inner: [4]
}
]
})
db.list.update(
{ "outer.key": "b" },
{ "$push": {
"outer.$.inner": {
"$each": [1,2,3], "$position": 0
}
}}
)
Again is as expected:
{
"outer" : [
{
"key" : "a",
"inner" : [
4
]
},
{
"key" : "b",
"inner" : [
1,
2,
3,
4
]
}
]
}
The interaction with specific drivers was already explained so there must be something different in the data, but if so then those statements are not valid.
And so exactly the same using Mongoose:
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
mongoose.connect('mongodb://localhost/nodetest');
var listSchema = new Schema({
});
var List = mongoose.model( "List", listSchema );
var Other = mongoose.model( "Other", listSchema );
List.collection.update(
{},
{ "$push": {
"list.sub": {
"$each": [ 1, 2, 3 ],
"$position": 0 }
}
},function(err,NumAffected) {
console.log("done");
}
);
Other.collection.update(
{ "outer.key": "b" },
{ "$push": {
"outer.$.inner": {
"$each": [ 1, 2, 3 ],
"$position": 0
}
}},function(err,NumAffected) {
console.log("done2")
}
);
I have found the code in the MongooseJS Model.update that omits support for the $position modifier.
In version 3.8.8, line 1928-1941 of query.js:
if ('$each' in val) {
obj[key] = {
$each: this._castUpdateVal(schema, val.$each, op)
}
if (val.$slice) {
obj[key].$slice = val.$slice | 0;
}
if (val.$sort) {
obj[key].$sort = val.$sort;
}
}
Here, obj[key] will be set to val.$each. You can see explicit support for setting the $slice & $sort modifiers, but the $position modifier will never be copied into obj[key].
So, although you may be able to by-pass the Model.update function of MongooseJS to directly access MongoDB's update function, it is clear that MongooseJS's Model.update function does not support the $position modifier.