dynamic mongodb query based on user preference in Meteor - mongodb

Based on the logged in user his/her preferences I want to create a collection and display in a view.
I'm in no way experienced with mongodb and now i'm ending up with this huge if/else statement and it's already slow (with 7 users in DB). But afaik it does give me the right results.
Meteor.publish('listprofiles', function () {
if ( ! this.userId ) return [];
var user = Meteor.users.findOne({ _id: this.userId }, {
fields : {
'profile.gender': 1,
'profile.preference': 1
}
}),
query;
user.gender = user.profile.gender;
user.preference = user.profile.preference;
if (user.gender === 'man') {
if (user.preference === 'straight') {
query = {
$and: [
{ 'profile.gender': 'woman' },
{
$or : [{ 'profile.preference' : 'straight' },
{ 'profile.preference' : 'bi' }]
}
]
};
} else if (user.preference === 'gay') {
query = {
$and: [
{ 'profile.gender': 'man' },
{
$or : [{ 'profile.preference' : 'gay' },
{ 'profile.preference' : 'bi' }]
},
]
};
} else if (user.preference === 'bi') {
query = {
$or: [
{
$and: [
{ 'profile.gender': 'man' },
{
$or : [{ 'profile.preference' : 'gay' },
{ 'profile.preference' : 'bi' }]
},
]
},
{
$and: [
{ 'profile.gender': 'woman' },
{
$or : [{ 'profile.preference' : 'straight' },
{ 'profile.preference' : 'bi' }]
}
]
}
]
};
}
The queries work, I tested them, but I'm unsure how to fit them dynamically. My guess is that query also shouldn't be an object, but I'm not sure how to create a valid variable..
var dbFindQuery = Meteor.users.find({
'profile.invisible': false,
queryShouldBeHereButObviouslyThisDoesNotWork
}, {
fields : {
'profile.name': 1,
'profile.city': 1,
'profile.country': 1,
'profile.gender': 1,
'profile.preference': 1,
'profile.story': 1
}
});
console.log(dbFindQuery.fetch());
return dbFindQuery;
anyone can give me a pointer in the right direction?

You can certainly factor out the common query objects. Here's one way to approach it:
Meteor.publish('listprofiles', function() {
if (!this.userId)
return [];
var user = Meteor.users.findOne(this.userId);
var gender = user.profile.gender;
var preference = user.profile.preference;
var straightOrBiWoman = {
'profile.gender': 'woman',
'profile.preference': {$in: ['straight', 'bi']}
};
var gayOrBiMan = {
'profile.gender': 'man',
'profile.preference': {$in: ['gay', 'bi']}
};
var query = {};
if (gender === 'man') {
switch (preference) {
case 'straight':
query = straightOrBiWoman;
break;
case 'gay':
query = gayOrBiMan;
break;
default:
query = {$or: [gayOrBiMan, straightOrBiWoman]};
}
}
query['profile.invisible'] = false;
return Meteor.users.find(query, {fields: {profile: 1}});
});
Here we are reusing straightOrBiWoman and gayOrBiMan based on the user's gender and preference. Note that I used the $in operator to simplify the query. I'd also suggest that you not specify 2nd-level fields in your fields modifier for reasons explained here. Finally, I'd recommend testing this code, as I may have missed something in the logic when rewriting it. Hopefully this example will help guide you in the right direction.

Related

how can I modify a field name / key in a nested array of objects in mongodb?

I have a mongodb collection with a number of objects like this:
{
"_id" : "1234",
"type" : "automatic",
"subtypes" : [
{
"_id" : "dfgd",
"name" : "test subtype",
"subjetRequired" : true,
},
{
"_id" : "dfgd",
"name" : "test subtype2",
"subjetRequired" : false,
}
],
"anotherField" : "some value"
}
As you can see, one of the keys in the subtypes array is incorrectly spelled - "subjetRequired" instead of "subjectRequired".
I want to correct that key name. How can I do that.
I'll preface this by saying I've not worked with mongodb very much in the past.
After a lot of researching, the best I could come up with is the following (which doesn't work):
function remap(doc) {
subtypes = doc.subtypes;
var count = 0;
subtypes.forEach(function(subtype){
db.taskType.update({"_id": subtype._id}, {
$set: {"subtypes.subjectRequired" : subtype.subjetRequired},
$unset: {"subtypes.subjetRequired": 1}
});
}
)
}
db.taskType.find({"subtypes.subjetRequired":{$ne:null}}).forEach(remap);
This doesn't work.
I know the loop is correct, as if I replace the other logic with print statements I can access and print the fields who's names I want to modify.
What am I doing wrong here?
You can use this update and avoid using any code, it's also stable so you can execute it multiple times with no fear.
db.collection.updateMany({
"subtypes.subjetRequired": {
$exists: true
}
},
[
{
$set: {
subtypes: {
$map: {
input: "$subtypes",
in: {
$mergeObjects: [
"$$this",
{
subjectRequired: "$$this.subjetRequired",
}
]
}
}
}
}
},
{
$unset: "subtypes.subjetRequired"
}
])
Mongo Playground
I could modify your loop to override the whole array of subtypes:
function remap(doc) {
correctSubtypes = doc.subtypes.map(({ subjetRequired, ...rest }) => ({
...rest,
subjectRequired: subjetRequired,
}));
var count = 0;
db.taskType.findByIdAndUpdate(doc._id, {
$set: {
subtypes: correctSubtypes,
},
});
}

Trying to iterate through a collection of MongoDb results to update another collection

I'm trying to update some documents from DB-collection1 (source db) over to DB-collection2 (destination DB) .. all on the same MongoDb (with same permissions, etc).
So for each document from DB-Collection1, update a specific document in DB-collectoin2, if it exists.
The documents in DB-collection1 have following shape:
{
"_id": {
"commentId": "082f3de6-a268-46b5-803f-89bafd172621"
},
"appliesTo": {
"targets": [
{
"_id": {
"documentId": "b1eb1ad5-e74c-4a64-a4f3-bdc67ba70b35"
},
"type": "Document"
}
]
}
}
And the matching document in DB-collection2 is:
{
"_id": {
"documentId": "b1eb1ad5-e74c-4a64-a4f3-bdc67ba70b35"
},
"name": "jill"
},
I'm using a cursor to iterate through the source collection but I'm not sure how I can do this?
This is the javascript code mongo shell script I'm trying right now, when I run the following command on a machine where mongo is installed:
CLI: root#f0cc2f13e70c:/src/scripts# mongo --host localhost --username root --password example copyFoosToBars.js
// copyFoosToBars.js
function main() {
print('Starting script.')
print()
var foosDb = db.getSiblingDB('foos');
var barsDb = db.getSiblingDB('bars');
// Grab all the 'foos' which have a some barId in some convoluted schema.
var sourceFoos = foosDb.getCollection('foos')
.find(
{
"appliesTo.targets.type" : "Document",
"_meta.deleted": null
},
{
"_id" : 0,
"appliesTo.targets._id.documentId" : 1
}
);
sourceFoos.forEach(function(foo){
// Check if this document exists in the bars-db
var desinationBars = barsDb.getCollection('bars')
.find(
{
"_id.documentId" : foo.appliesTo.targets._id.documentId,
},
);
printjson(desinationBars);
// If destinationBars document exists, then add a the field 'Text' : 'hi there' to the document -or- update the existing field, if the 'Text' field already exists in this document.
});
print()
print()
print('----------------------------------------------')
}
main()
So here's some sample json output for the first part of the query -> which proves that I have some data which passes that 'find/search' clause:
Starting script.
{
"appliesTo" : {
"targets" : [
{
"_id" : {
"barId" : "810e66e2-66d1-44f4-be0e-980309d8df8f"
}
}
]
}
}
{
"appliesTo" : {
"targets" : [
{
"_id" : {
"barId" : "54f25223-67bb-4d5d-ad47-24392e4acbdf"
}
}
]
}
}
{
"appliesTo" : {
"targets" : [
{
"_id" : {
"barId" : "34c83da3-eafd-41bf-93af-3a45d1644225"
}
}
]
}
}
This doesn't work.
MongoDB server version: 4.0.22
WARNING: shell and server versions do not match
Starting script.
uncaught exception: TypeError: comment.appliesTo.targets._id is undefined :
main/
<snip>
Can someone please suggest some clues as to how I can fix this, please?
First of all you need to safeguard against multiple items in the appliesTo.targets.
A document
{
"_id": {
"commentId": "082f3de6-a268-46b5-803f-89bafd172621"
},
"appliesTo": {
"targets": [
{
"_id": {
"documentId": "should-not-be-updated"
},
"type": "AnyOtherType"
},
{
"_id": {
"documentId": "b1eb1ad5-e74c-4a64-a4f3-bdc67ba70b35"
},
"type": "Document"
}
]
}
}
Will be selected by
.find(
{
"appliesTo.targets.type" : "Document",
"_meta.deleted": null
},
{
"_id" : 0,
"appliesTo.targets._id.documentId" : 1
}
);
with the resulting document:
{
"appliesTo": {
"targets": [
{
"_id": {
"documentId": "should-not-be-updated"
}
},
{
"_id": {
"documentId": "b1eb1ad5-e74c-4a64-a4f3-bdc67ba70b35"
}
}
]
}
}
so foo.appliesTo.targets[0]._id.documentId will be "should-not-be-updated".
Structure of the document does not allow to use $elemMatch, so you have to either use aggregation framework or filter the array clientside. The aggregation has benefit of running serverside and reduce amount of data to transfer to the client.
Secondly, there is no point to find documents from DB-collection2. You can update all matching ones straight away, like in "update...where" SQL .
So the code must be something like following:
var sourceFoos = db.foos.aggregate([
{
$unwind: "$appliesTo.targets"
},
{
$match: {
"appliesTo.targets.type": "Document",
"appliesTo.targets._id.documentId": {
$exists: true
},
"_meta.deleted": null
}
},
{
$project: {
_id: 0,
"documentId": "$appliesTo.targets._id.documentId"
}
}
]);
sourceFoos.forEach(function(foo){
db.bars.updateMany(
{"_id.documentId" : foo.documentId},
{$set:{'Text' : 'hi there'}}
)
})
If there are a lot of documents expected in the cursor I would recommend to look at bulk updates to speed it up, but as I mentioned earlier in this case mongo shell might not be an ideal tool.
target is an array so for accessing to the _id try like this :
foo.appliesTo.targets[0]._id.barId
use async/await with try/catch and use .toArray() after find query
// copyFoosToBars.js
async function main() {
try {
var foosDb = db.getSiblingDB("foos");
var barsDb = db.getSiblingDB("bars");
// Grab all the 'foos' which have a some barId in some convoluted schema.
var sourceFoos = await foosDb
.getCollection("foos")
.find(
{
"appliesTo.targets.type": "Bar",
"_meta.deleted": null,
},
{
_id: 0,
"appliesTo.targets._id.fooId": 1,
}
).toArray();
for (foo of sourceFoos) {
var desinationBars = await barsDb
.getCollection("bars")
.find({
"_id.barId": foo.appliesTo.targets[0]._id.barId,
})
.toArray();
console.log(desinationBars);
if(desinationBars.length> 0){
//do somthings
}
}
} catch (error) {
console.log(error)
}
}
in the first find query you select "appliesTo.targets._id.fooId" : 1so select ( fooId but in the result json show "barId" : "810e66e2-66d1-44f4-be0e-980309d8df8f" it's conflict, Anyway, I hope this solution solves your problem

Selecting and updating a nested object by it's ObjectId in Mongoose.js

I'm having trouble with something that thought would be trivial in MongoDB with Mongoose.
With a fairly simple schema like this
const UserSchema = new Schema({
groups: [
{
name: String,
members: [
{
hasAccepted: {
type: Boolean
}
}
]
}
]
});
When i create new groups, each member object gets an _id property of course. I simply want to select that member by its _id and update its hasAccepted property.
When I run a query with the _id of the member, I get back the entire record for the user, which makes it difficult to find the nested member to update it.
How can I trim the result down to just the member with the found ID and update its property?
I'm using Mongo 3.6.2 and have tried the new arrayFilters, but with no luck.
My code (using Node) is below, which returns the whole document, but with nothing updated.
const query = {
groups : {
$elemMatch : { members : { $elemMatch : {_id : <id>} } }
}
};
const update = {$set: {'groups.$[].members.$[o].hasAccepted':true }};
const options = { new: true, arrayFilters:[{"o._id":<id>}] };
// Find the document
User.findOneAndUpdate(query, update, options, function(error, result) {
if (error) {
res.send(error);
} else {
res.send(result);
}
});
EDIT: here's the full data from the test db i'm working with. The _id I've been testing with is one the for the member in Group 1: 5a753f168b5b7f0231ab0621
[
{
"_id": {
"$oid": "5a7505452f93de2c90f49a20"
},
"groups": [
{
"name": "Group 2",
"_id": {
"$oid": "5a7543b8e254ab02cd728c42"
},
"members": [
{
"user": {
"$oid": "5a7543b8e254ab02cd728c41"
},
"_id": {
"$oid": "5a7543b8e254ab02cd728c43"
},
"hasAccepted": false
}
]
},
{
"name": "Group 1",
"_id": {
"$oid": "5a753f168b5b7f0231ab0620"
},
"members": [
{
"user": {
"$oid": "5a753f168b5b7f0231ab061f"
},
"_id": {
"$oid": "5a753f168b5b7f0231ab0621"
},
"hasAccepted": false
}
]
}
]
},
{
"_id": {
"$oid": "5a753f168b5b7f0231ab061f"
},
"groups": [],
},
{
"_id": {
"$oid": "5a7543b8e254ab02cd728c41"
},
"groups": [],
}
]
Thanks for any help you can offer.
OK, so it turns out the the thing I needed to understand better are arrayFilters (that, and I needed to add the group name into the data I used to get to the value I needed to updated.
The thing that helped me understand arrayFilters the best was to think of the as a sort of subquery, like is used in the SQL world. Once I got that, I was able to figure out how to write my update.
This article was also very helpful in understanding how arrayFilters are used: http://thecodebarbarian.com/a-nodejs-perspective-on-mongodb-36-array-filters.html
Here's the code that worked for me. Note that you need Mongo 3.6 and Mongoose 5.0.0 to get support for arrayFilters.
Also, you need to be sure to require Mongoose's ObjectId like so
const ObjectId = require('mongoose').Types.ObjectId;
Here's the rest of the working code:
const query = {
groups : {
$elemMatch : { members : { $elemMatch : {_id : new ObjectId("theideofmymemberobject"), hasAccepted : false} } }
}
};
const update = {$set: {'groups.$[group].members.$[member].hasAccepted':true } };
const options = { arrayFilters: [{ 'group.name': 'Group 3' },{'member._id': new ObjectId("theideofmymemberobject")}] };
// update the document
User.update(query, update, options, function(error, result) {
if (error) {
res.send(error);
} else {
res.send(result);
}
});
You can try below aggregation to filter only the matching group and member from the group and member arrays
replace _id with the id to be searched, use the result to update the hasAccepted status
db.groups.aggregate(
[
{$addFields : {"groups" : {$arrayElemAt : [{$filter : {input : "$groups", as : "g", cond : {$in : [_id, "$$g.members._id"]}}}, 0]}}},
{$addFields : {"groups.members" : {$filter : {input : "$groups.members", as : "gm", cond : {$eq : [_id, "$$gm._id"]}}}}}
]
).pretty()
// An easier more modern way to do this is to pass a wildcard such as /:id in your API endpoint
// Use Object.assign()
// use the save() method
// If you are using JWT
// Otherwise find another unique identifier
const user = UserSchema.findOne({ id: req.user.id });
for (const oldObject of user.groups) {
if(oldObject.id === req.params.id) {
newObject = {
propertyName: req.body.val,
propertyName2: req.body.val2,
propertyName3: req.body.val3
}
// Update the oldObject
Object.assign(oldObject, newObject);
break;
}
}
user.save()
res.json(user);

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

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

$elemMatch equivalent in spring data mongodb

I need to know the equivalent code in spring data mongo db to the code below:-
db.inventory.find( {
qty: { $all: [
{ "$elemMatch" : { size: "M", num: { $gt: 50} } },
{ "$elemMatch" : { num : 100, color: "green" } }
] }
} )
I am able to get the answer. This can be done in Spring data mongodb using following code
Query query = new Query();
query.addCriteria(Criteria.where("qty").elemMatch(Criteria.where("size").is("M").and("num").gt(50).elemMatch(Criteria.where("num").is(100).and("color").is("green"))));
I think query in your answer generated below query
{ "qty" : { "$elemMatch" : { "num" : 100 , "color" : "green"}}}
I think thats not you need.
Its only check last elemMatch expression not all.
Try with this.
query = new Query();
Criteria first = Criteria.where("qty").elemMatch(Criteria.where("size").is("M").and("num").gt(50));
Criteria two = Criteria.where("qty").elemMatch(Criteria.where("num").is(100).and("color").is("green"));
query.addCriteria(new Criteria().andOperator(first, two));
Just implemented $all with $elemMatch with spring data Criteria API:
var elemMatch1 = new Criteria()
.elemMatch(Criteria.where("size").is("M").and("num").gt(50));
var elemMatch2 = new Criteria()
.elemMatch(Criteria.where("num").is(100).and("color").is("green"));
var criteria = Criteria.where("qty")
.all(elemMatch1.getCriteriaObject(), elemMatch2.getCriteriaObject());
mongoTemplate.find(Query.query(criteria), Inventory.class);
Note: important part is calling getCriteriaObject method inside Criteria.all(...) for each Criteria.elemMatch(...) element.
Just to make #Vaibhav answer a bit more clearer.
given Document in DB
{
"modified": true,
"items": [
{
"modified": true,
"created": false
},
{
"modified": false,
"created": false
},
{
"modified": true,
"created": true
}
]
}
You could do following Query if you need items where both attribute of an item are true.
Query query = new Query();
query.addCriteria(Criteria.where("modified").is(true));
query.addCriteria(Criteria.where("items")
.elemMatch(Criteria.where("modified").is(true)
.and("created").is(true)));
here an example how to query with OR in elemMatch
Query query = new Query();
query.addCriteria(Criteria.where("modified").is(true));
query.addCriteria(Criteria.where("items")
.elemMatch(new Criteria().orOperator(
Criteria.where("modified").is(true),
Criteria.where("created").is(true))));
Hi i implemented too in Kotlin, its is extended about if statements to create dynamic query :)
val map = segmentFilter.segmentValueMap.map {
val segmentCriteria = where("segment").isEqualTo(it.key)
if (it.value.isNotEmpty()) {
segmentCriteria.and("segmentValue").`in`(it.value)
}
if (sovOverviewInDateRange != null) {
segmentCriteria.and("date").lte(sovOverviewInDateRange.recent).gte(sovOverviewInDateRange.older)
}
Criteria().elemMatch(segmentCriteria)
}
criteriaQuery.and("sovOverview").all(map.map { it.criteriaObject })
sovOverviews: {
$all: [
{
$elemMatch: {
segment: "Velikost",
segmentValue: "S"
}
},
{
$elemMatch: {
segment: "Kategorie"
}
}
]
}