How to use $arrayElemAt operator in Spring Data MongoDB - spring-data-mongodb

$arrayElemAt new in MongoDB version 3.2.
db.users.aggregate([
{
$project:
{
name: 1,
first: { $arrayElemAt: [ "$favorites", 0 ] },
last: { $arrayElemAt: [ "$favorites", -1 ] }
}
}
])

you can now use $arrayElemAt with ArrayOperators.ArrayElemAt class.
The above MongoDB projection would be traduced like the following for Spring Data Mongo:
project("name")
.and(ArrayOperators.ArrayElemAt.arrayOf("favorites").elementAt(0)).as("first")
.and(ArrayOperators.ArrayElemAt.arrayOf("favorites").elementAt(-1)).as("last")

Even more concise, you can even use $arrayElementAt as follows:
project("name")
.and("favorites").arrayElementAt(0).as("first")
.and("favorites").arrayElementAt(-1).as("last")

DATAMONGO-1536 is about to add $arrayElemAt and other missing aggregation operators to Spring Data MongoDB for the Ingalls release. Meanwhile you can provide your own AggregationExpression to create whatever operation is needed.
For the above something like the following does the trick:
project("name") //
.and(context -> new BasicDBObject("$arrayElemAt", asList("$favorites", 0))).as("first")
.and(context -> new BasicDBObject("$arrayElemAt", asList("$favorites", -1))).as("last");

Related

MongoDB aggregation for finding an index of array nested object with $gt condition

I am trying to get the positional index of an array element (to push more elements at $position). I'm specifically targeting a single document (say, id=x)
Simplified document:
{
id:"x",
samples:[{"timestamp":123},{"timestamp":234},{"timestamp":345}}
}
Aggregation pipeline:
collection.aggregate([{"$match": {"id": "x"}},
{"$project": {"matchedIndex": {"$indexOfArray": ["$samples.timestamp", 234]}}}])
This works to return matchedIndex=1 .
Now I would like to find the index of 'samples' where timestamp is greater than say 235, which should return '2'
I have tried combinations of:
collection.aggregate([{"$match": {"UUID": "5fd41e35-5e49-O977-t091-6f228bc65e37"}},
{"$project": {"matchedIndex":
{"$indexOfArray": ["$samples.timestamp",
{"$gt": ["$$timestamp", 235]}]}}}])
I understand this wont work, I'm just not sure how to go about it.
The purpose is to insert many elements between timestamps. Maybe there is a better way entirely to do this. Thanks
Instead of using "$samples.timestamp" as first argument you can use $map there to transform it into an array of true and false values depending on your condition, try:
{
"$project": {
"matchedIndex": {
"$indexOfArray": [
{ $map: { input: "$samples.timestamp", in: { $gte: [ "$$this", 235 ] } } },
true
]
}
}
}
Mongo Playground

Mongo find position by id in an Array of Objects

I have a range of documents
{
_id: ObjectId("5e388da4df54cb8efb47e61b"),
userId:'test_user'
productId:'product_6_id'
recommendations:{
_id:123
rankedList:[
0:{id:ObjectId('product_5_id'),Name:'Product_5'},
1:{id:ObjectId('product_6_id'),Name:'Product_6'},
2:{id:ObjectId('product_3_id'),Name:'Product_3'}],
Date:'2020-02-25T05:03:55.439+00:00'
}
},
{
_id: ObjectId("5e388da4df54cb8efb47e62b"),
userId:'test_user1'
productId:'product_3_id'
recommendations:{
_id:123
rankedList:[
0:{id:ObjectId('product_1_id'),Name:'Product_1'},
1:{id:ObjectId('product_5_id'),Name:'Product_5'},
2:{id:ObjectId('product_3_id'),Name:'Product_3'}],
Date:'2020-02-25T05:03:55.439+00:00'
}
}
and I need to find each time the position of productId within the Array of objects rankedList.
Thus here the answer would be positionIndex=1 for first doc and positionIndex=2 for second document.
I am quite confused with $indexOfArray and how I should use it here with aggregate.
Yes, you need $indexOfArray. The tricky part is that recommendations.rankedList is an array of objects however MongoDB allows you to use following expression:
$recommendations.rankedList.id
which evaluates to a list of strings, ['product_5_id', 'product_6_id', 'product_3_id'] in this case so your code can look like this:
db.collection.aggregate([
{
$project: {
index: {
$indexOfArray: [ "$recommendations.rankedList.id", "$productId" ]
}
}
}
])
Mongo Playground

MongoDB, ObjectId's Array not recognized as array when using $nin

I'm trying to match documents based on the fact that their _id is not present in ObjectId's Array of their own.
Mongodb version: 3.2
Here's my query :
db.subscriptions.aggregate(
[
{ $match : { "device": { $exists: true } } },
{
$lookup : {
from: "devices",
localField: "device",
foreignField: "_id",
as: "device"
}
},
{ $match : { "device.0" : { $exists: true } } },
{ $unwind: "$device" },
{ $project : {"_id": 1, "subs": "$device.subscriptions" } },
{ $match: { "_id": { $in: this.subs } } }
]
)
I have two collection subscriptions and devices.
The document subscription have a property device which is an ObjectId refering to a device. The device document have an array of objectId refering to the subscriptions. It's a one-to-many relationship.
Step 1:
Matching subscription who actualy have the device property (it's not a clean clean db)
Step 2:
I'm performing a lookup to get my device document inside the subscription
Step 3:
Once again matching the fact that the lookup did give result
Step 4:
Unwind the device cause the lookup thing give me an array (i dont quite understand why tho')
Step 5:
Projecting the subscriptions array and the subscription id to work with simpler document:
{
"_id" : ObjectId("587e265cb9b235243c7f9960"),
"subs" : [
ObjectId("5899a894b1e11521a44a96f7"),
ObjectId("5a8c494db0bed60b389cb1da")
]
}
Step 6:
This is where the error appear, mongo's teling me this.subs is not an array so i cant use $nin => "errmsg" : "$in needs an array"
I've tried multiple format: "this.subs", "$subs", none of that works.
Can somebody helps me ?
Théo
Try below aggregation in 3.2. You can't compare the document field in $match stage. So you have to create a intermediate field (cmp) to hold the result from such comparison and $match stage to compare the result. Also there is no $in ( aggregation operator ). You have to use $setIsSubset to mimic such comparisons.
db.subscriptions.aggregate(
[
previous stages stages until $unwind
{$project:{
add all the fields you want in response,
"cmp":{$not:{$setIsSubset: [["$_id"], "$device.subscriptions"]}}
}},
{$match: {"cmp": true}}
]
)
Upgrade to 3.4 you can use $in(aggregation) operator and below pipeline.
db.subscriptions.aggregate(
[
previous stages stages until $unwind
{$project:{
add all the fields you want in response,
"cmp":{$not:{$in: ["$_id", "$device.subscriptions"]}}
}},
{$match: {"cmp": true}}
]
)
Upgrade to 3.6 you can use $in(aggregation) operator inside the $match with $expr which lets you use aggregation operators.
db.subscriptions.aggregate(
[
previous stages stages until $unwind
{$match: {"$expr": {$not:{$in: ["$_id", "$device.subscriptions"]}}}}
]
)

How do I create a query from an array with mongoose?

So I have this array with 3 IDs: ["56ba576f66b9add00e4d3082","56b9252266b9add00e4d3080","56b908f166b9add00e4d307f"]
I want to build a query that says something like that:
userModel.find({ $or:
[
{_id:"56ba576f66b9add00e4d3082"},
{_id:"56ba576f66b9add00e4d3082"},
{_id:"56ba576f66b9add00e4d3082"}
]},
{email:true});
Is there some sort of a method to use in order to produce such query with the array?
Should I be using mapReduce or the $where method or anything else? (I never used those before so I'm not sure if this is the case or not)
Perhaps I'm asking the wrong question? Any recommendations are very much welcome.
If you just have one element then just use $in:
var array = [
"56ba576f66b9add00e4d3082",
"56b9252266b9add00e4d3080",
"56b908f166b9add00e4d307f"
];
userModel.find({ "_id": { "$in": array } }, { "email": true });
An $in condition is basically an $or applied to the same field.
Try using the map function:
array = ["56ba576f66b9add00e4d3082","56b9252266b9add00e4d3080","56b908f166b9add00e4d307f"]
userModel.find({ $or: array.map(function(id) {
return {_id: id};
})}, {email:true});

Updating nested arrays in mongodb

I have a document in mongodb with 2 level deep nested array of objects that I need to update, something like this:
{
id: 1,
items: [
{
id: 2,
blocks: [
{
id: 3
txt: 'hello'
}
]
}
]
}
If there was only one level deep array I could use positional operator to update objects in it but for second level the only option I've came up is to use positional operator with nested object's index, like this:
db.objects.update({'items.id': 2}, {'$set': {'items.$.blocks.0.txt': 'hi'}})
This approach works but it seems dangerous to me since I'm building a web service and index number should come from client which can send say 100000 as index and this will force mongodb to create an array with 100000 indexes with null value.
Are there any other ways to update such nested objects where I can refer to object's ID instead of its position or maybe ways to check if supplied index is out of bounds before using it in query?
Here's the big question, do you need to leverage Mongo's "addToSet" and "push" operations? If you really plan to modify just individual items in the array, then you should probably build these arrays as objects.
Here's how I would structure this:
{
id: 1,
items:
{
"2" : { "blocks" : { "3" : { txt : 'hello' } } },
"5" : { "blocks" : { "1" : { txt : 'foo'}, "2" : { txt : 'bar'} } }
}
}
This basically transforms everything in to JSON objects instead of arrays. You lose the ability to use $push and $addToSet but I think this makes everything easier. For example, your query would look like this:
db.objects.update({'items.2': {$exists:true} }, {'$set': {'items.2.blocks.0.txt': 'hi'}})
You'll also notice that I've dumped the "IDs". When you're nesting things like this you can generally replace "ID" with simply using that number as an index. The "ID" concept is now implied.
This feature has been added in 3.6 with expressive updates.
db.objects.update( {id: 1 }, { $set: { 'items.$[itm].blocks.$[blk].txt': "hi", } }, { multi: false, arrayFilters: [ { 'itm.id': 2 }, { 'blk.id': 3} ] } )
The ids which you are using are linear number and it has to come from somewhere like an additional field such 'max_idx' or something similar.
This means one lookup for the id and then update. UUID/ObjectId can be used for ids which will ensure that you can use Distributed CRUD as well.
Building on Gates' answer, I came up with this solution which works with nested object arrays:
db.objects.updateOne({
["items.id"]: 2
}, {
$set: {
"items.$.blocks.$[block].txt": "hi",
},
}, {
arrayFilters: [{
"block.id": 3,
}],
});
MongoDB 3.6 added all positional operator $[] so if you know the id of block that need update, you can do something like:
db.objects.update({'items.blocks.id': id_here}, {'$set': {'items.$[].blocks.$.txt': 'hi'}})
db.col.update({"items.blocks.id": 3},
{ $set: {"items.$[].blocks.$[b].txt": "bonjour"}},
{ arrayFilters: [{"b.id": 3}] }
)
https://docs.mongodb.com/manual/reference/operator/update/positional-filtered/#update-nested-arrays-in-conjunction-with
This is pymongo function for find_one_and_update. I searched a lot to find the pymongo function. Hope this will be useful
find_one_and_update(filter, update, projection=None, sort=None, return_document=ReturnDocument.BEFORE, array_filters=None, hint=None, session=None, **kwargs)
Example
db.pymongo_object.find_one_and_update( filter = {'id' : 1}, update= {$set: {"items.$[array1].blocks.$[array2].txt": "hi"}}, array_filters =[{"array1.id" :2}, {"array2.id": 3}])
Also see pymongo documentation.

Resources