MongoDB $elemMatch updates the wrong element when an additional query is present - mongodb

I am facing a weird issue with MongoDB. I am using the official mongo client to use it from NodeJS.
My data sort of looks like this
// collection "products"
{
shop: 'ShopID1',
customer: ['CustomerID1', 'CustomerID2'],
products: [
{product: 'ProductID1', productCount: 10, customer: ['CustomerID1']},
{product: 'ProductID2', productCount: 5, customer: ['CustomerID2']},
]
}
Now, I want to remove 'CustomerID2' from the 2nd product object if it exists on both the outer customer array and in the inner customer array of the 2nd product object.
I can do it with the following query,
const productID = 'ProductID2';
const customerID = 'CustomerID2';
db
.collection('products')
.findOneAndUpdate(
{
shop: 'ShopID1',
// customer: {
// $in: [customerID]
// },
products: {
$elemMatch: {
product: productID,
customer: {
$in: [customerID]
}
}
},
},
{
$inc: {
'products.$.productCount': 1
},
$pull: {
'products.$.customer': customerID,
customer: customerID
}
},
);
The problem is, if I uncomment the commented part above, it updates the first product object, which is wrong. I can't find the reason why this happens as my knowledge of Mongo is limited. It seems to me the uncommented part is only adding an additional constraint that should not affect the update operation.
I would also appreciate any feedback on whether this is the correct way to achieve my stated goal. Thanks

//data preparation check
> db.custProducts.find().pretty();
{
"_id" : ObjectId("5f5e6d68598d922a1e6eff5c"),
"shop" : "ShopID1",
"customer" : [
"CustomerID1",
"CustomerID2"
],
"products" : [
{
"product" : "ProductID1",
"productCount" : 10,
"customer" : [
"CustomerID1"
]
},
{
"product" : "ProductID2",
"productCount" : 5,
"customer" : [
"CustomerID2"
]
}
]
}
//code to remove customerID2 field in both outer array and inner object
//.$. to be used to traverse the document object
> db.custProducts.aggregate([
... {$unwind:"$customer"},
... {$unwind:"$products"},
... {$match:{
... "shop": "ShopID1",
... "customer": "CustomerID2",
... "products.product": "ProductID2"
... }
... }
... ]).forEach(function(doc){
... print("prod: ",doc.products.product);
... print("cust: ",doc.customer);
... db.custProducts.update(
... {"shop": doc.shop, "products.product": "ProductID2" },
... {$pull:
... {
... customer:{$in:[doc.customer]},
... "products.$.customer":{$in:[doc.customer]}
... }
... }
... );
... }
... );
prod: ProductID2
cust: CustomerID2
//check the output of the update execution.
> db.custProducts.find().pretty();
{
"_id" : ObjectId("5f5e6d68598d922a1e6eff5c"),
"shop" : "ShopID1",
"customer" : [
"CustomerID1"
],
"products" : [
{
"product" : "ProductID1",
"productCount" : 10,
"customer" : [
"CustomerID1"
]
},
{
"product" : "ProductID2",
"productCount" : 5,
"customer" : [ ]
}
]
}
>
//further,you can add other logic like $inc in the above code

Related

How to update Meteor array element inside a document

I have a Meteor Mongo document as shown below
{
"_id" : "zFndWBZTvZPgSKXHP",
"activityId" : "aRDABihAYFoAW7jbC",
"activityTitle" : "Test Mongo Document",
"users" : [
{
"id" : "b1#gmail.com",
"type" : "free"
},
{
"id" : "JqKvymryNaCjjKrAR",
"type" : "free"
},
],
}
I want to update a specific array element's email with custom generated id using Meteor query something like the below.
for instance, I want to update the document
if 'users.id' == "b1#gmail.com" then update it to users.id = 'SomeIDXXX'
So updated document should looks like below.
{
"_id" : "zFndWBZTvZPgSKXHP",
"activityId" : "aRDABihAYFoAW7jbC",
"activityTitle" : "Test Mongo Document",
"users" : [
{
"id" : "SomeIDXXX",
"type" : "free"
},
{
"id" : "JqKvymryNaCjjKrAR",
"type" : "free"
},
],
}
I have tried the below but didnt work.
Divisions.update(
{ activityId: activityId, "users.id": emailId },
{ $set: { "users": { id: _id } } }
);
Can someone help me with the relevant Meteor query ? Thanks !
Your query is actually almost right except for a small part where we want to identify the element to be updated by its index.
Divisions.update({
"activityId": "aRDABihAYFoAW7jbC",
"users.id": "b1#gmail.com"
}, {
$set: {"users.$.id": "b2#gmail.com"}
})
You might need the arrayFilters option.
Divisions.update(
{ activityId: activityId },
{ $set: { "users.$[elem].id": "SomeIDXXX" } },
{ arrayFilters: [ { "elem.id": "b1#gmail.com" } ], multi: true }
);
https://docs.mongodb.com/manual/reference/operator/update/positional-filtered/
You need to use the $push operator instead of $set.
{ $push: { <field1>: <value1>, ... } }

MongoDB: How to get the object names in collection?

and think you in advance for the help. I have recently started using mongoDB for some personal project and I'm interested in finding a better way to query my data.
My question is: I have the following collection:
{
"_id" : ObjectId("5dbd77f7a204d21119cfc758"),
"Toyota" : {
"Founder" : "Kiichiro Toyoda",
"Founded" : "28 August 1937",
"Subsidiaries" : [
"Lexus",
"Daihatsu",
"Subaru",
"Hino"
]
}
}
{
"_id" : ObjectId("5dbd78d3a204d21119cfc759"),
"Volkswagen" : {
"Founder" : "German Labour Front",
"Founded" : "28 May 1937",
"Subsidiaries" : [
"Audi",
"Volkswagen",
"Skoda",
"SEAT"
]
}
}
I want to get the object name for example here I want to return
[Toyota, Volkswagen]
I have use this method
var names = {}
db.cars.find().forEach(function(doc){Object.keys(doc).forEach(function(key){names[key]=1})});
names;
which gave me the following result:
{ "_id" : 1, "Toyota" : 1, "Volkswagen" : 1 }
however, is there a better way to get the same result and also to just return the names of the objects. Thank you.
I would suggest you to change the schema design to be something like:
{
_id: ...,
company: {
name: 'Volkswagen',
founder: ...,
subsidiaries: ...,
...<other fields>...
}
You can then use the aggregation framework to achieve a similar result:
> db.test.find()
{ "_id" : 0, "company" : { "name" : "Volkswagen", "founder" : "German Labour Front" } }
{ "_id" : 1, "company" : { "name" : "Toyota", "founder" : "Kiichiro Toyoda" } }
> db.test.aggregate([ {$group: {_id: null, companies: {$push: '$company.name'}}} ])
{ "_id" : null, "companies" : [ "Volkswagen", "Toyota" ] }
For more details, see:
Aggregation framework
$group
Accumulator operators
As a bonus, you can create an index on the company.name field, whereas you cannot create an index on varying field names like in your example.

Update existing mongodb data into an embedded document

I am new to MongoDB so this is probably a basic question (hopefully). I currently have 10 million records with 410 fields loaded in a mongodb collection like so:
{
"_id" : ObjectId("........"),
"AddressID" : 123455,
"IndividualId" : 1,
"personfirstname" : "FirstName",
"personmiddleinitial" : "M",
"personlastname" : "LastName",
"etc": "....."
}
I need to wrap all of this data into an embedded document like so:
{
"_id" : ObjectId("........"),
"data" : {
"AddressID" : 123455,
"IndividualId" : 1,
"personfirstname" : "FirstName",
"personmiddleinitial" : "M",
"personlastname" : "LastName",
"etc": "....."
}
I don't necessarily need to update this data in-place but that would be nice. If I need to export this data somehow specifying the new format and then re-import the new, updated data that is fine. Performing this via the MongoDB shell would be ideal.
As suggested by chridam within comments you can execute the following aggregation pipeline:
db.collectionName.aggregate([
{ $project: { _id: "$_id", data: "$$ROOT" } },
{ $out: "newCollectionName" }
]);
This way you have the _id field both at root level and in the data object. Thus, you can execute a massive update to unset the second one:
db.newCollectionName.updateMany(
{},
{ $unset: { "data._id": "" } }
);
Finally, you can drop the first collection and rename the second to restore the original name on the updated collection:
db.collectionName.drop();
db.newCollectionName.rename("collectionName");
This approach fully works within the database, avoiding fetching any of your 10 million documents.
You can simply do this in the shell with the following
db.test.find().forEach(function(doc){
doc = { _id: doc._id, data: doc };
delete doc.data._id;
db.test.save(doc);
});
For example, if we insert the following documents:
> db.test.insertMany([
... {
... _id: ObjectId("5a91af8908e17c5997e03b7e"),
... field1: false,
... field2: 0,
... field3: "No"
... },
... {
... _id: ObjectId("5a91afbc08e17c5997e03b7f"),
... field1: true,
... field2: 1,
... field3: "Yes"
... }])
{
"acknowledged" : true,
"insertedIds" : [
ObjectId("5a91af8908e17c5997e03b7e"),
ObjectId("5a91afbc08e17c5997e03b7f")
]
}
Then run:
db.test.find().forEach(function(doc){
doc = { _id: doc._id, data: doc };
delete doc.data._id;
db.test.save(doc);
});
Our documents now look like this:
> db.test.find().pretty()
{
"_id" : ObjectId("5a91af8908e17c5997e03b7e"),
"data" : {
"field1" : false,
"field2" : 0,
"field3" : "No"
}
}
{
"_id" : ObjectId("5a91afbc08e17c5997e03b7f"),
"data" : {
"field1" : true,
"field2" : 1,
"field3" : "Yes"
}
}

Mongoose match element or empty array with $in statement

I'm trying to select any documents where privacy settings match the provided ones and any documents which do not have any privacy settings (i.e. public).
Current behavior is that if I have a schema with an array of object ids referenced to another collection:
privacy: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Category',
index: true,
required: true,
default: []
}],
And I want to filter all content for my categories and the public ones, in our case content that does not have a privacy settings. i.e. an empty array []
We currently query that with an or query
{"$or":[
{"privacy": {"$size": 0}},
{"privacy": {"$in":
["5745bdd4b896d4f4367558b4","5745bd9bb896d4f4367558b2"]}
}
]}
I would love to query it by only providing an empty array [] as one the comparison options in the $in statement. Which is possible in mongodb:
db.emptyarray.insert({a:1})
db.emptyarray.insert({a:2, b:null})
db.emptyarray.insert({a:2, b:[]})
db.emptyarray.insert({a:3, b:["perm1"]})
db.emptyarray.insert({a:3, b:["perm1", "perm2"]})
db.emptyarray.insert({a:3, b:["perm1", "perm2", []]})
> db.emptyarray.find({b:[]})
{ "_id" : ObjectId("5a305f3dd89e8a887e629ce0"), "a" : 2, "b" : [ ] }
{ "_id" : ObjectId("5a305f3dd89e8a887e629ce3"), "a" : 3, "b" : [ "perm1", "perm2", [ ] ] }
> db.emptyarray.find({b:{$in:[]}})
> db.emptyarray.find({b:{$in:[[], "perm1"]}})
{ "_id" : ObjectId("5a305f3dd89e8a887e629ce0"), "a" : 2, "b" : [ ] }
{ "_id" : ObjectId("5a305f3dd89e8a887e629ce1"), "a" : 3, "b" : [ "perm1" ] }
{ "_id" : ObjectId("5a305f3dd89e8a887e629ce2"), "a" : 3, "b" : [ "perm1", "perm2" ] }
{ "_id" : ObjectId("5a305f3dd89e8a887e629ce3"), "a" : 3, "b" : [ "perm1", "perm2", [ ] ] }
> db.emptyarray.find({b:{$in:[[], "perm1", null]}})
{ "_id" : ObjectId("5a305f3dd89e8a887e629cde"), "a" : 1 }
{ "_id" : ObjectId("5a305f3dd89e8a887e629cdf"), "a" : 2, "b" : null }
{ "_id" : ObjectId("5a305f3dd89e8a887e629ce0"), "a" : 2, "b" : [ ] }
{ "_id" : ObjectId("5a305f3dd89e8a887e629ce1"), "a" : 3, "b" : [ "perm1" ] }
{ "_id" : ObjectId("5a305f3dd89e8a887e629ce2"), "a" : 3, "b" : [ "perm1", "perm2" ] }
{ "_id" : ObjectId("5a305f3dd89e8a887e629ce3"), "a" : 3, "b" : [ "perm1", "perm2", [ ] ] }
> db.emptyarray.find({b:{$in:[[]]}})
{ "_id" : ObjectId("5a305f3dd89e8a887e629ce0"), "a" : 2, "b" : [ ] }
{ "_id" : ObjectId("5a305f3dd89e8a887e629ce3"), "a" : 3, "b" : [ "perm1", "perm2", [ ] ] }
Maybe like this:
"privacy_locations":{
"$in": ["5745bdd4b896d4f4367558b4","5745bd9bb896d4f4367558b2",[]]
}
But this query, works from the console (CLI), but not in the code where it throws a cast error:
{
"message":"Error in retrieving records from db.",
"error":
{
"message":"Cast to ObjectId failed for value \"[]\" at ...
}
}
Now I perfectly understand the cast is happening because the Schema is defined as an ObjectId.
But I still find that this approach is missing two possible scenarios.
I believe it is possible to query (in MongoDB) null options or empty array within an $in statement.
array: {$in:[null, [], [option-1, option-2]}
Is this correct?
I've been thinking that the best solution to my problem (Cannot select in options or empty) could be to have empty arrays be an array with a fix option of ALL for example. A setting for privacy that means ALL instead of how it is now which is that if not set, that is considered all.
But I don't want a major refactor of the existing code, I just need to see if I can make a better query or more performant query.
Today we have the query working with an $OR statement that has issues with indexes. And even if it is fast, I wanted to bring attention to this issue even if is not considered a bug.
I will appreciate any comments or guidance.
The semi-short answer is that the schema is mixing types for the privacy property (ObjectId and Array) while declaring that it is strictly of type ObjectId in the schema.
Since MongoDB is schema-less it will allow any document shape per document and doesn't need to verify the query document to match a schema. Mongoose on the other hand is meant to apply a schema enforcement and so it will verify a query document against the schema before it attempts to query the DB. The query document for { privacy: { $in: [[]] } } will fail validation since an empty array is not a valid ObjectId as indicated by the error.
The schema would need to declare the type as Mixed (which doesn't support ref) to continue using an empty array as an acceptable type as well as ObjectId.
// Current
const FooSchema = new mongoose.Schema({
privacy: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Category',
index: true,
required: true,
default: []
}]
});
const Foo = connection.model('Foo', FooSchema);
const foo1 = new Foo();
const foo2 = new Foo({privacy: [mongoose.Types.ObjectId()]});
Promise.all([
foo1.save(),
foo2.save()
]).then((results) => {
console.log('Saved', results);
/*
[
{ __v: 0, _id: 5a36e36a01e1b77cba8bd12f, privacy: [] },
{ __v: 0, _id: 5a36e36a01e1b77cba8bd131, privacy: [ 5a36e36a01e1b77cba8bd130 ] }
]
*/
return Foo.find({privacy: { $in: [[]] }}).exec();
}).then((results) => {
// Never gets here
console.log('Found', results);
}).catch((err) => {
console.log(err);
// { [CastError: Cast to ObjectId failed for value "[]" at path "privacy" for model "Foo"] }
});
And the working version. Also note the adjustment to properly apply the required flag, index flag and default value.
// Updated
const FooSchema = new mongoose.Schema({
privacy: {
type: [{
type: mongoose.Schema.Types.Mixed
}],
index: true,
required: true,
default: [[]]
}
});
const Foo = connection.model('Foo', FooSchema);
const foo1 = new Foo();
const foo2 = new Foo({
privacy: [mongoose.Types.ObjectId()]
});
Promise.all([
foo1.save(),
foo2.save()
]).then((results) => {
console.log(results);
/*
[
{ __v: 0, _id: 5a36f01733704f7e58c0bf9a, privacy: [ [] ] },
{ __v: 0, _id: 5a36f01733704f7e58c0bf9c, privacy: [ 5a36f01733704f7e58c0bf9b ] }
]
*/
return Foo.find().where({
privacy: { $in: [[]] }
}).exec();
}).then((results) => {
console.log(results);
// [ { _id: 5a36f01733704f7e58c0bf9a, __v: 0, privacy: [ [] ] } ]
});

How can I query ancestral relationships in MongoDB?

I'm reading the documentation for tree structures here:
http://docs.mongodb.org/manual/tutorial/model-tree-structures/
The hierarchy is:
Books -> Programming -> [Languages, Databases -> [Postgres, MongoDB]]
In the documentation it says:
The query to retrieve the parent of a node is fast and straightforward:
db.categories.findOne( { _id: "MongoDB" } ).parent
That makes sense. However, how can I run a query based on attributes of the ancestor? For example, instead of just retrieving the parent, suppose I wish to find all documents where the grandfather has an _id of "Books" how would I do that? The answer should be "Languages" and "Databases".
If your tree structure is fixed. You can query by following way, before that, just change your MongoDB structure by following URL,
http://docs.mongodb.org/manual/tutorial/model-tree-structures-with-nested-sets/
to get all ancestor of Databases,
var databaseCategory = db.categories.findOne( { _id: "Databases" } );
db.categories.find( { left: { $lt: databaseCategory.left }, right: { $gt: databaseCategory.right } } );
Result:
{ "_id" : "Books", "parent" : 0, "left" : 1, "right" : 12 }
{ "_id" : "Programming", "parent" : "Books", "left" : 2, "right" : 11 }
to get all descendant of Databases,
var databaseCategory = db.categories.findOne( { _id: "Databases" } );
db.categories.find( { left: { $gt: databaseCategory.left }, right: { $lt: databaseCategory.right } } );
Result:
{ "_id" : "MongoDB", "parent" : "Databases", "left" : 6, "right" : 7 }
{ "_id" : "dbm", "parent" : "Databases", "left" : 8, "right" : 9 }