Mongodb - Array containing element and not containing another one - mongodb

I'm trying to query one of my mongodb collection that looks like this:
> db.collection.find()
{name: "foo", things: [{stuff:"banana", value: 1}, {stuff:"apple", value: 2}, {stuff:"strawberry", value: 3}]}
{name: "bar", things: [{stuff:"banana", value: 4}, {stuff:"pear", value: 5}]}
...
My goal is to list all the object that have the things field containing an element with stuff=banana but no stuff=apple
I tried something like this:
db.transactions.find({
"things": {
$elemMatch: {
"stuff": "banana",
$ne: {
"stuff": "apple"
}
}
}
)
But it's not working. Any ideas?

The below query will get the list of all documents that have the things field containing an element with stuff=banana but no stuff=apple:
db.test.find({"$and":[{"things.stuff":"banana"}, {"things.stuff":{$ne:"apple"}}]})

Use the $not and $and operators:
db.collection.find({
$and:[
{"things": {$elemMatch: { "stuff": "banana" }}},
{"things": {$not: {$elemMatch: { "stuff": "apple"}}}}
]
});

Use $in and $nin to include and exclude
db.transactions.find({
"things.stuff": {
$in: ["banana"],
$nin:["apple"]
}
})

Related

MongoDB (mongoose) possible to $elemMatch inside $cond OR $in a partially matched object?

So, this question stems from another question I asked ( MongoDB update with conditional addToSet/pull )
Essentially in that SO question, I found that I couldn't use the typical $in syntax if my array is an array of objects.
Typical syntax for conditionally updating a field:
$cond: {
$in: [{element}, $arr], // Need to provide the element Exactly, including _id if one exists
$setDifference: [$arr, [{element}]]
$concactArrays: [$arr, [{element}]]
}
The reason $in doesn't work is that if _id is auto-generated, you won't be able to match it for the $in or the $setDifference. The solution I arrived at uses $filter, but is fragile in that the array needs to at least be initialed a default [] (otherwise $size will error out on a null array)
On to my question.
Is there a way to make $in or $elemMatch work in this scenario?
I couldn't get the following to work (says $elemMatch is an unrecognized expression:
$cond: {
$in: [arr: {$elemMatch: {element}}, $arr],
Full Example
Mongo Object
{_id: 1, message: 'text1'}
{_id: 2, message: 'text2', reactions: [{_id: autoGen1, user: 'bob', reaction:'👍'},
{_id: autoGen2, user: 'bob', reaction:'👎'}
{_id: autoGen3, user: 'meg', reaction:'😵'}]}
Update Query
db.collection.update(
{},
[
{
$set: {
reactions: {
$cond: [
{
$in: [
{
reactions: { $elemMatch: {user: "bob", reaction: "good"}}
},
"$reactions"
]
},
"1",
"2"
]
}
}
}
])
https://mongoplayground.net/p/Ig0TKMAMNkI

How do I query nested objects within an array to match an $in array in mongodb?

Data structure
{
users: [
{id: "aaa"},
{id: "bbb"},
{id: "ccc"}
]
},
{
users: [
{id: "111"},
{id: "222"},
{id: "333"}
]
},
array: ["111", "222", "333"]
I want to get the document where every "id" matches my array, like $in does. But I don't want matches where only two of three matches. So in this case, the query should return the second document.
One way:
Query an array for an element
const cursor = db.collection('inventory').find({
users: [{id: "111"},{id: "222"},{id: "333"}]
});
https://docs.mongodb.com/manual/tutorial/query-arrays/#query-an-array-for-an-element
Related Q:
MongoDB Find Exact Array Match but order doesn't matter
const userIds = ["abc", "123", ...]
I found this query to solve my problem.
{
$and: [
{users: {$elemMatch: {id: {$in: userIds}}}},
{users: {$size: userIds.length}}
]
}

MongoDb Access array of objects with certain property

I have one document as follows:
{
user: 'hvt07',
photos: [
{
link: 'http://link.to.com/image1.jpg',
isPrivate: true
},
{
link: 'http://link.to.com/image2.jpg',
isPrivate: false
}
]
}
I want to get all photos which are with:
isPrivate: false
I am using the following query:
db.collection_name.find({ photos:{ $elemMatch:{isPrivate: false} } }).pretty()
I have also tried:
db.collection_name.find({'photos.isPrivate': true}).pretty()
But both return all elements in the array even ones that are set as :
isPrivate: true
Please suggest.
Aggregation is the solution.
You need to deconstruct the photos array using the $unwind operator. Next use the $match to select documents where isPrivate: false. The $group you can regroup your documents by _id and reconstruct your photos array using the $push operator
db.collection_name.aggregate(
[
{$unwind: "$photos"},
{$match: {"photos.isPrivate": false}},
{$group: {"_id": {"id": "$_id", "user": "$user"}, photos: {$push: "$photos"}}}
{$project: {"_id": "$_id.id", "user": "$_id.user", "photos": 1, "_id": 0 }}
]
)
You can use $elemMatch to the result projection like this
db.collection_name.find(
{ photos:{ $elemMatch:{isPrivate: false} } }, //1
{photos:{$elemMatch:{isPrivate: false}}}) //2
Find all documents that have at least a photo which is not private
Select only photos that are not private for the documents found.

Add attribute to one element in mongo array (based on another property of that array item)

I've got a series of mongoDB documents that follow this structure:
{
"_id": "myId",
"myArray": [
{"foo": "bar"},
{"foo": "baz"}
]
}
Another might look like this:
{
"_id": "myOtherId",
"myArray": [
{"foo": "bar"},
{"foo": "baz"}
]
}
I need to add a property to any of the array elements where foo=bar only if the id=myOtherId ... in other words, so my second document will look like this (but the first document won't be updated):
{
"_id": "myOtherId",
"myArray": [
{"foo": "bar","foot":"bart"},
{"foo": "baz"}
]
}
I know that the first argument of my update would be something like this:
db.coll.update({$and:[{"_id":"myOtherId"},{"myArray.foo":"bar"}]}, // but what comes in the $set?
How do I then compose the $set argument so that {"$foot":"bart"} is added only where I need it? Is there a way to capture the index value of the array item, or a better way to handle it?
You don't need to use $and in this case as there's always an implicit AND between multiple terms in a query object. You can also use the $ positional update operator in your $set to identity the matched myArray element from your query to update:
db.coll.update(
{_id: "myOtherId", "myArray.foo": "bar"},
{$set: {"myArray.$.foot": "bart"}})
Building up on the answer from #JohnnyHK.
In case there are multiple elements in array myArray with {"foo": "bar"}
{
"_id": "myOtherId",
"myArray": [
{"foo": "bar"},
{"foo": "baz"},
{"foo": "bar"}
]
}
and all the matching elements have to be udpated then we need to use arrayFilters with update command.
db.coll.update(
{_id: "myOtherId", "myArray.foo": "bar"},
{$set: {"myArray.$[elem].foot": "bart"}},
{
multi: true,
arrayFilters: [ { "elem.foo": "bar" } ]
})

Better Alternative than $all with $elemMatch

Here's a sample document of my Mongo DB structure:
{ _id: 1
records: [{n: "Name", v: "Kevin"},
{n: "Age", v: "100"},
...,
{n: "Field25", v: "Value25"} ]
}
To search on all documents having Name of "Kevin" and an Age of "100", I'm using $all with $elemMatch. I need to use $elemMatch's for an exact sub-document match of n: "Name" and v: "Kevin", as well as $all since I'm querying on an array.
db.collection.find({"records" : { $all: [
{$elemMatch: {n: "Name", v: "Kevin"},
{$elemMatch: {n: "Age", v: "100"}
]}})
However, the $all operator is inefficient when the first $elemMatch argument is non-selective, i.e. there are many documents that match this field.
The Mongo Docs elaborate:
In the current release, queries that use the $all operator must scan
all the documents that match the first element in the query array. As
a result, even with an index to support the query, the operation may
be long running, particularly when the first element in the array is
not very selective.
Is there a better alternative for my queries?
I would suggest an radical change to your structure, and this would simplify the queries. Sorry if this change is not possible, but without more data I do not see any problem with that:
{ _id: 1
records: [{"Name":"Kevin",
"Age":"100",
...,
"Field25":"Value25"} ]
}
And the query:
db.collection.find("records":{$elemMatch:{Name:"Kevin","Age":100}})
This will return all the objects that have a record (assuming there are more records, for example if they are students of a class) matching all of the conditions mentioned.
But in case you want to have just a document per _id, forget about the records array:
{ _id: 1,
"Name":"Kevin",
"Age":"100",
...,
"Field25":"Value25"} ]
}
and the query:
db.collection.find({Name:"Kevin","Age":100})
Hope this helps.
I think this should be the best solution.
db.collection.find({},
{
records: {
$filter: {
input: "$records",
as: "record",
cond: {
$or: [
{
$eq: [
"$$record.v",
"Kevin"
]
},
{
$eq: [
"$$record.v",
"100"
]
}
]
}
}
}
})
solution link: https://mongoplayground.net/p/aQAw0cG9Ipm