Mongodb find() only include non-empty arrays - javascript

What I'm trying to achieve with a find query is to only include "someArray"s if it's inner array is not empty. For example the JSON below:
{
"document": "some document",
"someArray": [
{
"innerArray": [
"not empty"
]
},
{
"innerArray": [
[] //empty
]
}
]
}
Would return this:
{
"document": "some document",
"someArray": [
{
"innerArray": [
"not empty"
]
}
]
}
I'm using the following find:
Visit.find({'someArray.innerArray.0': {$exists: true}}, function(err, data){});
However, this returns all data.
Have also tried:
Visit.find({}, {'someArray.innerArray': {$gt: 0}}, function(err, data) {});
But this returns nothing
Any ideas on how to approach this?
Cheers

The general case here to check for a non-empty array is to check to see if the "first" element actually exists. For single matches you can project with the positional $ operator:
Vist.find(
{ "someArray.innerArray.0": { "$exists": true } },
{ "document": 1,"someArray.$": 1},
function(err,data) {
}
);
If you need more than a single match or have arrays nested more deeply than this, then the aggregation framework is what you need to handle the harder projection and/or "filter" the array results for more than one match:
Visit.aggregate(
[
// Match documents that "contain" the match
{ "$match": {
"someArray.innerArray.0": { "$exists": true }
}},
// Unwind the array documents
{ "$unwind": "$someArray" },
// Match the array documents
{ "$match": {
"someArray.innerArray.0": { "$exists": true }
}},
// Group back to form
{ "$group": {
"_id": "$_id",
"document": { "$first": "$document" },
"someArray": { "$push": "$someArray" }
}}
],function(err,data) {
}
)
Worth noting here that you are calling this "empty" but in fact is is not, as it actually contains another empty array. You probably don't want to do that with real data, but if you have then you would need to filter like this:
Visit.aggregate(
[
{ "$match": {
"someArray": { "$elemMatch": { "innerArray.0": { "$ne": [] } } }
}},
{ "$unwind": "$someArray" },
{ "$match": {
"someArray.innerArray.0": { "$ne": [] }
}},
{ "$group": {
"_id": "$_id",
"document": { "$first": "$document" },
"someArray": { "$push": "$someArray" }
}}
],function(err,data) {
}
);

Related

Get a single sub-object from a nested object array with MongoDB

I have a node.js server using express and mongoose and I also have a Json structure of :
[
{
"_id": "63dbaf5daabee478202ae59f",
"experimentId": "85a91abe-ef2f-416f-aa13-ec1bdf5d9766",
"experimentData": [
{
"scanId": 1652890241,
"scanData": [
{
"areaName": "A1",
"areaData": [],
"_id": "63dbaf94aabee478202ae5a5"
},
...
],
"_id": "63dbaf6caabee478202ae5a3"
},
...
],
"__v": 2
},
...
]
How can I create a query to return a single object from the scanData array like
{
"areaName": "A1",
"areaData": [],
"_id": "63dbb006e322869df811eea4"
}
The best I was able to do was:
// * Get all shapes in a well/area
// ! uses the experiment id, scan id and areaName
router.get("/:id/scan/:scanId/area/:areaName", async (req, res) => {
try {
const experiment = await Experiment.findOne({
experimentId: req.params.id,
experimentData: {
$elemMatch: {
scanId: req.params.scanId,
scanData: {
$elemMatch: {
areaName: req.params.areaName
}
}
}
}
}, {'experimentData.scanData.$': 1})
console.log(experiment)
if (!experiment || experiment.length === 0) res.status(404).json({})
else {
res.send(experiment.experimentData[0])
}
} catch (err) {
res.status(500).json({ message: err.message })
}
})
But that just returned the scanData array it would be great if I could go one level deeper and just get the object with the areaName.
I also tried some solutions with $aggregate but was not able to get any data displayed it kept returning an empty array
You can $match by your criteria layer-by-layer and $unwind to get the final scanData object in an aggregation pipeline. Use $replaceRoot to get only the scanData object.
db.collection.aggregate([
{
"$match": {
"experimentId": "85a91abe-ef2f-416f-aa13-ec1bdf5d9766"
}
},
{
"$unwind": "$experimentData"
},
{
"$match": {
"experimentData.scanId": 1652890241
}
},
{
"$unwind": "$experimentData.scanData"
},
{
"$match": {
"experimentData.scanData.areaName": "A1"
}
},
{
"$replaceRoot": {
"newRoot": "$experimentData.scanData"
}
}
])
Mongo Playground

Mongoose get objects which match an object item from an array of objects

I would like to find a single document matching the courseID but inside the document only objects from the materials array whose moduleNo matches the one I give. But the query I currently use seems to return the correct document but also returns all the objects in materials array. Any help would be appreciated.
My schema,
const materialSchema = new mongoose.Schema({
courseID: String,
materials: [
{
moduleNo: Number,
moduleMaterial: String,
},
],
});
My code/query,
exports.getMaterials = (req, res) => {
const { courseID, moduleNo } = req.query;
Material.findOne(
{ courseID, "materials.moduleNo": moduleNo },
(err, result) => {
if (err) {
console.error(err);
} else {
res.json(result);
}
}
);
};
Use the $elemMatch operator, something lik this:
exports.getMaterials = (req, res) => {
const { courseID, moduleNo } = req.query;
Material.findOne(
{ courseID },
{"materials": { $elemMatch: {moduleNo: moduleNo}},
(err, result) => {
if (err) {
console.error(err);
} else {
res.json(result);
}
}
);
};
Update: To return all matching elements in the array, you will have to use an aggregation pipeline, having $filter stage, to filter out array elements. Like this:
exports.getMaterials = (req, res) => {
const { courseID, moduleNo } = req.query;
Material.aggregate(
[
{
$match: {
courseID: courseID
}
},
{
"$project": {
courseID: 1,
materials: {
"$filter": {
"input": "$materials",
"as": "material",
"cond": {
"$eq": [
"$$material.moduleNo",
moduleNo
]
}
}
}
}
}
]
);
};
Here's the playground link.
Way 1 : Use $elemMatch operator in project field
The $elemMatch operator limits the contents of an array field from
the query results to contain only the first element matching the
$elemMatch condition
Result : Returns only one matching array element
syntax :
db.collection.find(query,projection)
db.collection.find({
"field": field_value
},{
"array_name":{
$elemMatch:{"key_name": value }
},
field:1,
field_2:1,
field_3:0
})
https://mongoplayground.net/p/HKT1Gop32Pq
Way 2 : Array Field Limitations array.$ in project field
*
Result : Returns only one matching array element
db.collection.find({
"field": field_value,
"array_name.key_name": value
},{
"array_name.$":1,
field:1,
field_2:1,
field_3:0
});
https://mongoplayground.net/p/Db0azCakQg9
Update : Using MongoDB Aggregation
Result : Returns multiple matching array elements
db.collection.aggregate([
{
"$unwind": "$materials"
},
{
"$match": {
"courseID": "Demo",
"materials.moduleNo": 1
}
}
])
will return output as :
[
{
"_id": ObjectId("5a934e000102030405000000"),
"courseID": "Demo",
"materials": {
"moduleMaterial": "A",
"moduleNo": 1
}
},
{
"_id": ObjectId("5a934e000102030405000000"),
"courseID": "Demo",
"materials": {
"moduleMaterial": "B",
"moduleNo": 1
}
}
]
And If you want to format output :
db.collection.aggregate([
{
"$unwind": "$materials"
},
{
"$match": {
"courseID": "Demo",
"materials.moduleNo": 1
}
},
{
"$group": {
"_id": {
"courseID": "$courseID",
"moduleNo": "$materials.moduleNo"
},
"materials": {
"$push": "$materials.moduleMaterial"
}
}
},
{
"$project": {
"_id": 0,
"courseID": "$_id.courseID",
"moduleNo": "$_id.moduleNo",
"materials": "$materials"
}
}
])
will return result as :
[
{
"courseID": "Demo",
"materials": [
"A",
"B"
],
"moduleNo": 1
}
]
https://mongoplayground.net/p/vdPVbce6WkX

how to use lookup on array object mongodb

I'm new on mongodb. so I try design the schema for my collection is like below
all the ObjectId is not real
stockIn documents
{
serial:"stk0001",
date:'2021-06-11',
productInTransation:[
{
_id:"60ae220b066b8d9861118cb1",
productId:"60ae220b066b8d9861118cb2"
qty:2
},
{
_id:"60ae220b066b8d9861118cb1",
productId:"60ae220b066b8d9861118cb1",
qty:2
}
]
}
and I have a products collection
[
{
_id:"60ae220b066b8d9861118cb5",
name:"sepatu"
},
{
_id:"60ae220b066b8d9861118cb4",
name:"sendal"
}
]
so what I expect from those documents is just like below
{
serial:"stk0001",
date:'2021-06-11',
productInTransation:[
{
_id:"60ae220b066b8d9861118cb1",
productId:"60ae220b066b8d9861118cb2"
qty:2,
product:
{
_id:"60ae220b066b8d9861118cb5",
name:"sepatu"
},
},
{
_id:"60ae220b066b8d9861118cb1",
productId:"60ae220b066b8d9861118cb1",
qty:2,
product:
{
_id:"60ae220b066b8d9861118cb4",
name:"sendal"
}
}
]
}
this collection is just simplified from the real case.
and the problem I don't know how to do a query on mongodb, so the output will same as the expected. thank's for any help
You can use $lookup
$unwind to deconstruct the array
$lookup to join collections
$ifNull to make sure this doesn't give any NPE when we take from first element from the joined array using $arrayElemAt
$group to reconstruct the array
Here is the code
db.stockIn.aggregate([
{ $unwind: "$productInTransation" },
{
"$lookup": {
"from": "products",
"localField": "productInTransation.productId",
"foreignField": "_id",
"as": "productInTransation.product"
}
},
{
"$addFields": {
"productInTransation.product": {
"$ifNull": [ { "$arrayElemAt": [ "$productInTransation.product", 0 ] }, [] ]
}
}
},
{
"$group": {
"_id": "$_id",
"date": { "$first": "$date" },
"serial": { "$first": "$serial" },
"productInTransation": { $push: "$productInTransation" }
}
}
])
Working Mongo playground

How do I get all attributes which are numeric types in mongo db?

I need to extract all the attributes which are of numeric types. For example, if the different attributes are
{
age: 32
gender: "female"
year: 2020
name: "Abc"
}
My query should return ["age","year"]
I think the below query should help you out.
db.test.aggregate([
// Remove this `$limit` stage if your Collection schema is dynamic and you want to process all the documents instead of just one
{
"$limit": 1
},
{
"$project": {
"arrayofkeyvalue": {
"$filter": {
"input": {"$objectToArray":"$$ROOT"},
"as": "keyValPairs",
"cond": {
"$in": [{"$type": "$$keyValPairs.v"}, ["double", "int", "long"]],
// Change the above line to the following to get only `int` keys instead of `int, double` and `long`:
// "$eq": [{"$type": "$$keyValPairs.v"}, "int"],
}
}
}
}
},
{
"$group": {
"_id": null,
"unique": {"$addToSet": "$arrayofkeyvalue.k"}
}
},
{
"$project": {
"_id": 0,
"intKeyNames": {
"$reduce": {
input: "$unique",
initialValue: [],
in: {$setUnion : ["$$value", "$$this"]}
}
}
}
},
])
The above query result will be something like this:
{
"intKeyNames" : [
"_id",
"abc",
"paymentMonth",
"paymentYear",
"value"
]
}

How can I remove a duplicate object from a MongoDB array?

My data looks like this:
{
"foo_list": [
{
"id": "98aa4987-d812-4aba-ac20-92d1079f87b2",
"name": "Foo 1",
"slug": "foo-1"
},
{
"id": "98aa4987-d812-4aba-ac20-92d1079f87b2",
"name": "Foo 1",
"slug": "foo-1"
},
{
"id": "157569ec-abab-4bfb-b732-55e9c8f4a57d",
"name": "Foo 3",
"slug": "foo-3"
}
]
}
Where foo_list is a field in a model called Bar. Notice that the first and second objects in the array are complete duplicates.
Aside from the obvious solution of switching to PostgresSQL, what MongoDB query can I run to remove duplicate entries from foo_list?
Similar answers that do not quite cut it:
https://stackoverflow.com/a/16907596/432
https://stackoverflow.com/a/18804460/432
These questions answer the question if the array had bare strings in it. However in my situation the array is filled with objects.
I hope it is clear that I am not interested querying the database; I want the duplicates to be gone from the database forever.
Purely from an aggregation framework point of view there are a few approaches to this.
You can either just apply $setUnion in modern releases:
db.collection.aggregate([
{ "$project": {
"foo_list": { "$setUnion": [ "$foo_list", "$foo_list" ] }
}}
])
Or more traditionally with $unwind and $addToSet:
db.collection.aggregate([
{ "$unwind": "$foo_list" },
{ "$group": {
"_id": "$_id",
"foo_list": { "$addToSet": "$foo_list" }
}}
])
Or if you were just interested in the duplicates only then by general grouping:
db.collection.aggregate([
{ "$unwind": "$foo_list" },
{ "$group": {
"_id": {
"_id": "$_id",
"foo_list": "$foo_list"
},
"count": { "$sum": 1 }
}},
{ "$match": { "count": { "$ne": 1 } } },
{ "$group": {
"_id": "$_id._id",
"foo_list": { "$push": "$_id.foo_list" }
}}
])
The last form could be useful to you if you actually want to "remove" the duplicates from your data with another update statement as it identifies the elements which are duplicates.
So in that last form the returned result from your sample data identifies the duplicate:
{
"_id" : ObjectId("53f5f7314ffa9b02cf01c076"),
"foo_list" : [
{
"id" : "98aa4987-d812-4aba-ac20-92d1079f87b2",
"name" : "Foo 1",
"slug" : "foo-1"
}
]
}
Where results are returned from your collection per document that contains duplicate entries in the array and which entries are duplicated. This is the information you need to update, and you loop the results as you need to specify the update information from the results in order to remove duplicates.
This is actually done with two update statements per document, as a simple $pull operation would remove "both" items, which is not what you want:
var cursor = db.collection.aggregate([
{ "$unwind": "$foo_list" },
{ "$group": {
"_id": {
"_id": "$_id",
"foo_list": "$foo_list"
},
"count": { "$sum": 1 }
}},
{ "$match": { "count": { "$ne": 1 } } },
{ "$group": {
"_id": "$_id._id",
"foo_list": { "$push": "$_id.foo_list" }
}}
])
var batch = db.collection.initializeOrderedBulkOp();
var count = 0;
cursor.forEach(function(doc) {
doc.foo_list.forEach(function(dup) {
batch.find({ "_id": doc._id, "foo_list": { "$elemMatch": dup } }).updateOne({
"$unset": { "foo_list.$": "" }
});
batch.find({ "_id": doc._id }).updateOne({
"$pull": { "foo_list": null }
});
});
count++;
if ( count % 500 == 0 ) {
batch.execute();
batch = db.collection.initializeOrderedBulkOp();
}
});
if ( count % 500 != 0 ) {
batch.execute();
}
That's the modern MongoDB 2.6 and above way to do it, with a cursor result from aggregation and Bulk operations for updates. But the principles remain the same:
Identify the duplicates in documents
Loop the results to issue the updates to the affected documents
Use $unset with the positional $ operator to set the "first" matched array element to null
Use $pull to remove the null entry from the array
So after processing the above operations your sample now looks like this:
{
"_id" : ObjectId("53f5f7314ffa9b02cf01c076"),
"foo_list" : [
{
"id" : "98aa4987-d812-4aba-ac20-92d1079f87b2",
"name" : "Foo 1",
"slug" : "foo-1"
},
{
"id" : "157569ec-abab-4bfb-b732-55e9c8f4a57d",
"name" : "Foo 3",
"slug" : "foo-3"
}
]
}
The duplicate is removed with the "duplicated" item still intact. That is how you process to identify and remove the duplicate data from your collection.

Categories

Resources