mongodb check an element has one nested attribute - mongodb

I have a collection of documents like this
[
{ "name": "pika", "attrs": { "A": 1, "B": 2 ... } },
{ "name": "chu", "attrs": { "C": 3 } },
{ "name": "plop", "attrs": { "A": 1, "C": 3 } }
]
I would like to delete records that have a "C" and only a "C" attribute in their "attrs" (line named "chu") using mongodb 2.4. The number of possible attributes under the attrs key is possibly large (> 100).
I can use several queries.
How would you do that ?
Edit : I want to keep attr C in lines containing other attributes.

You have two choices. If your key space is small you can do
db.collection.remove( {C : {$exists:true}, A: {$exists:false}, B: {$exists: false} })
Otherwise you'll need to do
var col = db.collection.find( {C : {$exists:true}} );
for(doc in col) {
var found = false
for(key in obj) {
if( key !== 'C' ) {
found = true;
break;
}
}
if(found === false) {
db.collection.remove(doc);
}
}
There's no way to count the number of keys in a document directly within MongoDB and there's no way to query on wildcards in key names (e.g. you can't do "key not equal to C : {$exists: false}"). So you either need to test all keys explicitly or test each document in your application layer.

If the "attrs" is a array, in other words, you collections like this:
{ "name": "pika", "attrs": [{ "A": 1}, {"B": 2}] };
{ "name": "chu", "attrs": [{ "C": 3 }] };
{ "name": "plop", "attrs": [{ "A": 1}, {"C": 3 }] }
Then you can write a query like below to find the specific record you want:
db.entities.find({"attrs.C": {$exists: true}, "attrs": {$size: 1}});
You can check the mongodb website to find the $size operation, http://docs.mongodb.org/manual/reference/operator/size/

Related

MongoDB update array if condition matches

I'm looking for a MongoDB aggregation pipeline which updates an array with a conditional statement.
My data looks like the following:
{
"_id": 1,
"locations": [
{
"controllerID": 1,
"timestamp": 1234
},
{
"controllerID": 2,
"timestamp": 2342
}
]
},...
Potential new entry:
{
"controllerID": 2,
"timestamp": //will be set automatically
}
At first I want to match the _id (not a problem) and then push the new entry to the locations array if the element with the newest/latest timestamp has a different controllerID.
When pushing a new location object the timestamp will be set automatically.
Example 1
Input:
{
"controllerID": 2,
}
Expected Result:
{
"_id": 1,
"locations": [
{
"controllerID": 1,
"timestamp": 1234
},
{
"controllerID": 2,
"timestamp": 2342
}//noting is added because the newset entry in the array has the same controllerID
]
},
Example 2
Input:
{
"controllerID": 1,
}
Expected Result:
{
"_id": 1,
"locations": [
{
"controllerID": 1,
"timestamp": 1234
},
{
"controllerID": 2,
"timestamp": 2342
},
{//added because the controllerID is different to te last element
"controllerID": 1,
"timestamp": 4356
}
]
},
Thanks in advance!
Here's a solution.
var candidate = 2;
rc=db.foo.update({}, // add matching criteria here; for now, match ALL
[
[
// We cannot say "if condition then set fld = X else do nothing".
// We must say "set fld to something based on condition."
// The common pattern becomes:
// "Set fld to (if condition then X else fld)"
// in other words, set the fld to *itself*
//
// Note the use of the dot operator on the $locations field.
// Also, not sure about what sort of new timestamp is desired so let's
// just throw in an ISODate() for now.
{$set: {'locations': {$cond: [
{$ne:[candidate, {$last:'$locations.controllerID'}]}, // IF not same as candidate...
{$concatArrays: ['$locations',
// $concatArrays wants arrays, not objects, so we must wrap our new
// object with [] to make an array of 1:
[ {controllerId:candidate,timestamp:new ISODate() } ]
]}, // THEN concat a new entry to end of existing locations
'$locations' // ELSE just set back to existing locations
]}
}}
],
{multi:true}
);
The engine is "smart enough" to realize that setting a field to itself will not trigger a modification so the approach is performant and will not rewrite the entire set of matched objects; this can be seen in the output of the update() call, e.g.:
printjson(rc);
{ "nMatched" : 1002, "nUpserted" : 0, "nModified" : 1 }

How to insert subdocument into array field only if there's no document with the same "key": "value" pair (MongoDB)?

I have a collection of documents that look like this:
{
"AAA": 1,
"BBB": [
{
"CCC": 1,
"DDD": [1,2,3]
}
]
}
How to insert a new subdocument ({"CCC": 1, "DDD": []}) into "BBB" array only if there's no object with {"CCC": 1} key pair?
You can actually do this in a couple of ways, the easiest would be to make the query 'fail' to match if the document has CCC: 1, like so:
db.collection.updateOne(
{
_id: docId,
'BBB.CCC': {
$ne: 1,
},
},
{
'$push': {
BBB: {
'CCC': 1,
'DDD': [],
},
},
},
);
Now if the document has a BBB.CCC value of 1 then the update will not find a document to update and nothing will be updated as you expect.
Mongo Playground

How to query nested heterogeneous document

I have an array that have this format:
data{
[sequentialId]{guid:value1,name:value2}
}
I need to do something like db.data.find("data.?.name":"value1")
All the solutions I've seen don't work because they are expecting me to know the name of the attribute, but in this case the ID is sequential.
I've looked at the standard way of querying nested documents described here
https://docs.mongodb.com/manual/tutorial/query-array-of-documents/
I also looked at some options like $unwind but I can't get anything to work
here's a small sample
{"41":{"b":453081600,"f":1,"h":171,"s":4,"w":4,"wr":[2,0]},
"80":{"b":337132800,"f":2,"h":169,"s":4,"w":4,"wr":[0,0]},
"388":{"b":148694400,"f":1,"h":188,"l":{"c":[{"e":2001,"g":13,"m":292,"s":1992,"t":18},{"e":2006,"g":11,"m":197,"s":2001,"t":1},{"e":2009,"g":2,"m":111,"s":2006,"t":1790},{"e":2009,"g":0,"m":1,"s":2009,"t":1937},{"e":2010,"g":1,"m":14,"s":2010,"t":1},{"e":2011,"g":0,"m":8,"s":2010,"t":13}],"n":[{"e":2007,"g":1,"m":73,"s":1996,"t":1318}]},"s":2,"w":3,"wr":[0,0]}}
for example in this set of data I might need to query all the docs where "f"=1
The following query can get us the expected output:
Note: We are fetching the documents which have name is equal to a
db.collection.aggregate([
{
$addFields:{
"filtered":{
$filter:{
"input":"$data",
"as":"info",
"cond":{
$let:{
"vars":{
"array":{
$objectToArray:"$$info"
}
},
"in":{
$in:["a","$$array.v.name"]
}
}
}
}
}
}
},
{
$match:{
"filtered.0":{
$exists:true
}
}
},
{
$project:{
"filtered":0
}
}
]).pretty()
Data set:
{
"data":[
{
"41": {
"b": 453081600,
"name": "a"
},
"80": {
"b": 337132800,
"name": "b"
},
"388": {
"b": 148694400,
"name": "c"
}
}
]
}
{
"data":[
{
"41": {
"b": 453081600,
"name": "b"
},
"80": {
"b": 337132800,
"name": "b"
},
"388": {
"b": 148694400,
"name": "c"
}
}
]
}
Output:
{
"data":[
{
"41": {
"b": 453081600,
"name": "a"
},
"80": {
"b": 337132800,
"name": "b"
},
"388": {
"b": 148694400,
"name": "c"
}
}
]
}
Query analysis:
We are creating a filtered array to hold only those records from data array which contains name equal to a inside any subdocument with an unknown key.
It's done by first converting each document of data into an array of key-value pairs. Now we can directly query the values without worrying about the keys.
Finally, filter those documents in which the size of filtered array is greater than zero.

How can I return only one field in a mongo array of objects without removing rest of object

I am querying a collection and in that collection I have an array of objects. In that array I want to use projection to only return one field in every object. However, I do not want to remove all the data outside that object.
Let's say one document in the collection looks like this
{
"a": "some val",
"b": "some other val",
"c": [
{
"d": "some array val",
"e": "some other array val"
}
]
}
And let's say I want to remove every field in the array besides 'd' and end up with
{
"a": "some val",
"b": "some other val",
"c": [
{
"d": "some array val",
}
]
}
I tried:
db.collection.find({}, { "c.d": 1 })
but this removed "a" and "b" as well and just returned:
{
"c": [
{
"d": "some array val",
}
]
}
Also, I cannot do:
db.collection.find({}, { "c.e": 0 })
because there may be other fields besides 'e' and those should be hidden as well.
You can run $addFields to overwrite existing field and $map to transform c array and take only d values, try:
db.collection.aggregate([
{
$addFields: {
c: {
$map: {
input: "$c",
in: { d: "$$this.d" }
}
}
}
}
])

Retrieving a subset of data from MongoDB

If I have a collection similar to:
[
{ "test": [ { "a": 1, "b": 2 }, { "a": 10, "b": 1 } ] },
{ "test": [ { "a": 5, "b": 1 }, { "a": 14, "b": 2 } ] },
...
]
How do I obtain only a subset of data consisting of the a values when b is 2? In SQL, this would be something similar to:
SELECT test.a FROM collection WHERE test.b = 2
I do understand that I can limit what data I get with something like:
collection.find({ }, { "test.a": 1 })
But that returns all the a values. How can I limit it so that it returns only the values in which b is 2 (the WHERE test.b = 2 part of the SQL equivalent)?
You can do this by adding a selector object as the first parameter of your find call and using the $elemMatch projection operator:
collection.find({ 'test.b': 2 }, { test: { $elemMatch: { b: 2 } }, 'test.a': 1 })
But this will only return the first test array element per-doc where b is 2. You would need to use the aggregation framework if there can be multiple b:2 elements in your test arrays.