MongoDB: Checking if nested array contains sub-array - node.js

I have a use case where the database is modelled like this:
name: XYZ
gradeCards: [{
id: 1234, // id of the report card
comments: ['GOOD','NICE','WOW']
}, {
id: 2345,
comments: ['GOOD','NICE TRY']
}]
Now, I have a query that I would like to query the schema as follows:
I would be given a list of ids and values.
For example: the given list is as follows:
[{
id: 1234,
comments: ['GOOD','NICE']
},{
id: 2345,
comments: ['GOOD']
}]
In short the ID should be matching and the comments should be a sub-array of the comments array for that id and also, all the conditions specified in the array should be matched, so it should be an AND condition on all the conditions provided.
I was able to get to this query, but it matches all of the elements in the comments array, I want that id should be exactly matched and comments should be a subarray.
For matching all elements in comments array:
db.getCollection('user').find({arr:{
$all: [{
id:1234,
comments:['GOOD','NICE','WOW']
},{
id:2345,
comments:['GOOD','NICE TRY']
}]
}})

You can try $all with $elemMatch to match on the query conditions.
db.collection.find({
gradeCards: {
$all: [{
"$elemMatch": {
id: 1234,
comments: {
$in: ['GOOD', 'NICE']
}
}
}, {
"$elemMatch": {
id: 2345,
comments: {
$in: ['GOOD']
}
}
}, ]
}
})

Related

How to prevent duplicates in MongoDB even if its coming from other field?

My schema contains two types of mobile numbers, stored as:
{
phoneCode: String,
phoneNumber: String,
mobileCode: String,
mobileNumber: String
}
How can I handle duplicity while storing or updating data if:
there's a phoneCode + phoneNumber or mobileCode + mobileNumber combination already stored in either of phone or mobile fields.
For example, lets say my database has two documents:
[
{ phoneCode: "+1", phoneNumber: "5556667898", mobileCode: "+1", mobileNumber: "5556668899" },
{ phoneCode: "+1", phoneNumber: "6655777898", mobileCode: "+1", mobileNumber: "6665557788"},
]
And I try to update first document with: mobileCode: +1, mobileNumber: 6665557788.
It should be prevented as this combination already exist in second document's mobile code and number field.
I can write query to check before store or update like:
const { phoneCode, phoneNumber, mobileCode, mobileNumber } = req.body;
collection.findOne({
$or:[
{ $and: [{ phoneCode }, { phoneNumber} ] },
{ $and: [{ mobileCode }, { mobileNumber }] },
{ $and: [{ phoneCode : mobileCode }, { phoneNumber : mobileNumber}] },
{ $and: [{ mobileCode: phoneCode }, { mobileNumber: phoneNumber }] }
]
})
But this does not feel optimize way, I want to handle it, so even if I have to add a third number I can handle that as well.
By using the Attribute Pattern, you can move this subset of information into an array and reduce the indexing needs also. You turn this information into an array of key-value pairs:
{
contacts: [{ code: String, number: String, type: String }]
}
For example, lets say your database has two documents:
[
{
contacts: [
{ code: "+1", number: "5556667898", type: "phone" },
{ code: "+1", number: "5556668899", type: "mobile" }
]
},
{
contacts: [
{ code: "+1", number: "6655777898", type: "phone" },
{ code: "+1", number: "6665557788", type: "mobile" }
]
}
]
Your query for single pair matching would be:
collection.findOne({
"contacts.code": "+1",
"contacts.number": "6665557788"
})
Playground
Your query for multiple pair matching would be:
const { phoneCode, phoneNumber, mobileCode, mobileNumber } = req.body;
collection.findOne({
"contacts.code": { $in: [phoneCode, mobileCode] },
"contacts.number": { $in: [phoneNumber, mobileNumber] }
})
Playground
For more optimisation you can use indexes in contacts fields,
You can use compound multi key index,
For a compound multikey index, each indexed document can have at most one indexed field whose value is an array,
collection.createIndex({ "contacts.code": 1, "contacts.number": 1 });
If your goal is to prevent duplicates during insert, perhaps a different DB design would be better e.g...
yourDb.phoneNumbers
yourDb.contacts
phoneNumbers: {
"_id": "phonenumber in string without spaces, dashes, brackets e.g. +123456789",
"contact": <contact ID>,
}
contacts: {
"_id": UUID,
"phoneNumbers": [
{
"type": "e.g. mobile",
"number": "same as in phoneNumbers collection",
"display number": "number with formatting",
...
},
...
]
}
// Usage:
1. Create random UUID
2. Create phoneNumber document and try to insert into phoneNumbers (use above ID for contact)
3. If successful, create contact document and insert into contacts (user above ID for _id)
This design enables fast inserts (only requires checking PKs) and, as a bonus, you can use the phoneNumbers collection to quickly find contacts by phone number.
If your goal is to find duplicates in DB, but not prevent them, you can use your current structure and use the aggregation framework to find duplicates by projecting all types of phone numbers into a single phoneNumber string field + contactId field, then group by phoneNumber, collect contactId into array per phoneNumber, plus count of contactIds (countIds), filter where countIds > 1, then you have a list of phoneNumbers that are duplicates, together with contactIds that are sharing them.

Mongoose group documents by value but only return latest document each

I have a collection called "Words", where I save different Words in different languages. Words are stored in "content" and their language code in "lang" (en, de, fr).
This is my Words schema:
content: {
type: String
},
lang: {
type: String,
enum: ['en', 'de', 'fr']
}
I am now trying to retrieve the latest stored value for each language, only returning one document each.
This is my desired example output:
[{
lang: "en",
content: "Whats up"
},{
lang: "de",
content: "Guten Tag"
},{
lang: "fr",
content: "Salut"
}]
I've already tried to use aggregate function with group. But now the two letter language code gets returned as the document id:
Words.aggregate([{
$group: {
_id: '$lang'
}
}]
Words.aggregate([{$sort: {'_id': -1}},
{$group: {_id:'$lang',
word: {
$push: {
_id: '$_id',
content: '$content'
}
}
}},
{$project: {
_id:0,
lang:'$_id',
content: {$arrayElemAt:['$word.content',0]}
}}])
First, I used sort on _id by descending order. (Assuming you had use mongoDB auto-generated _id)
Next, group all the content by language and lastly project the first content which is the latest according to _id.

MongoDB Searching if element exist in array

So I have something like Survey Schema (I am using mongoose).
In this Schema, for each voting option, I have votes[] array that contains ObjectIds of Users.
Now I want to check if User can vote again, or if he already voted?
The simple solution is iterating thru votes with .indexOf() and checking if id exists. Now this is a blocking way for Node JS, since this operation is sync.
Is there a way to do this with Mongo aggregate or query? So for each voting option I would get additional field like:
didIVoted: true
My Schema looks like this:
const SurveySchema = new Schema({
title: {
type: String
},
options: [{
value: String,
votes: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }]
}]
}, { timestamps: true })
You can use $addFields and $map to overwrite existing options field. To check if userId exists in votes array you can use $indexOfArray
SurveySchema.aggregate([
{
$addFields: {
options: {
$map: {
input: "$options",
in: {
value: "$$this.value",
votes: "$$this.votes",
didIVote: { $ne: [ { $indexOfArray: [ "$$this.votes", userId ] }, -1 ] }
}
}
}
}
}
])

Mongoose delete operation [duplicate]

Here is array structure
contact: {
phone: [
{
number: "+1786543589455",
place: "New Jersey",
createdAt: ""
}
{
number: "+1986543589455",
place: "Houston",
createdAt: ""
}
]
}
Here I only know the mongo id(_id) and phone number(+1786543589455) and I need to remove that whole corresponding array element from document. i.e zero indexed element in phone array is matched with phone number and need to remove the corresponding array element.
contact: {
phone: [
{
number: "+1986543589455",
place: "Houston",
createdAt: ""
}
]
}
I tried with following update method
collection.update(
{ _id: id, 'contact.phone': '+1786543589455' },
{ $unset: { 'contact.phone.$.number': '+1786543589455'} }
);
But it removes number: +1786543589455 from inner array object, not zero indexed element in phone array. Tried with pull also without a success.
How to remove the array element in mongodb?
Try the following query:
collection.update(
{ _id: id },
{ $pull: { 'contact.phone': { number: '+1786543589455' } } }
);
It will find document with the given _id and remove the phone +1786543589455 from its contact.phone array.
You can use $unset to unset the value in the array (set it to null), but not to remove it completely.
You can simply use $pull to remove a sub-document.
The $pull operator removes from an existing array all instances of a value or values that match a specified condition.
Collection.update({
_id: parentDocumentId
}, {
$pull: {
subDocument: {
_id: SubDocumentId
}
}
});
This will find your parent document against given ID and then will remove the element from subDocument which matched the given criteria.
Read more about pull here.
In Mongoose:
from the document:
To remove a document from a subdocument array we may pass an object
with a matching _id.
contact.phone.pull({ _id: itemId }) // remove
contact.phone.pull(itemId); // this also works
See Leonid Beschastny's answer for the correct answer.
To remove all array elements irrespective of any given id, use this:
collection.update(
{ },
{ $pull: { 'contact.phone': { number: '+1786543589455' } } }
);
To remove all matching array elements from a specific document:
collection.update(
{ _id: id },
{ $pull: { 'contact.phone': { number: '+1786543589455' } } }
);
To remove all matching array elements from all documents:
collection.updateMany(
{ },
{ $pull: { 'contact.phone': { number: '+1786543589455' } } }
);
Given the following document in the profiles collection:
{ _id: 1, votes: [ 3, 5, 6, 7, 7, 8 ] }
The following operation will remove all items from the votes array that are greater than or equal to ($gte) 6:
db.profiles.update( { _id: 1 }, { $pull: { votes: { $gte: 6 } } } )
After the update operation, the document only has values less than 6:
{ _id: 1, votes: [ 3, 5 ] }
If you multiple items the same value, you should use $pullAll instead of $pull.
In the question having a multiple contact numbers the same use this:
collection.update(
{ _id: id },
{ $pullAll: { 'contact.phone': { number: '+1786543589455' } } }
);
it will delete every item that matches that number. in contact phone
Try reading the manual.

Execute query and grab result in two subsections mongodb

I'm using mongoose to deal with my database.
I have the following model:
var DeviceSchema = mongoose.Schema({
type: Number,
pushId: String
});
The type attribute can be either 0 or 1.
I want to execute a query that grab all documents and retrieve the result in the following format:
{
fstType: [
{
_id: "545d533e2c21b900000ad234",
type: 0,
pushId: "123"
},
{
_id: "545d533e2c21b900000ad235",
type: 0,
pushId: "124"
},
],
sndType: [
{
_id: "545d533e2c21b900000ad236",
type: 1,
pushId: "125"
},
{
_id: "545d533e2c21b900000ad237",
type: 1,
pushId: "126"
},
]
}
Is that possible? I want to do that in one single query.
Thanks.
Is that possible? I want to do that in one single query.
Yes. It is possible. You can achieve the desired result, through the following aggregation pipeline operations.
Sort by the type parameter in ascending order.
Group records together having the same type, construct an array of
documents for each group. After this stage, only two records will be
present, each with an attribute called items, which is an array of
records for each group.
Since our records are sorted by type, the first group will contain
records with type 0, and the second with type 1.
At last we merge the groups and give them each a name, based on their type.
var model = mongoose.model('collection',DeviceSchema);
model.aggregate([
{$sort:{"type":-1}},
{$group:{"_id":"$type",
"items":{$push:"$$ROOT"},
"type":{$first:"$type"}}},
{$project:{"items":{$cond:[{$eq:["$type",0]},
{"firstType":"$items"},
{"secondType":"$items"}]}}},
{$group:{"_id":null,
"firstType":{$first:"$items.firstType"},
"secondType":{$last:"$items.secondType"}}},
{$project:{"_id":0,"firstType":1,"secondType":1}}
], function (err, result) {
if (err) {
console.log(err);
return;
}
console.log(result);
});
o/p:
{ firstType:
[ { _id: '545d533e2c21b900000ad234', type: 0, pushId: '123' },
{ _id: '545d533e2c21b900000ad235', type: 0, pushId: '124' } ],
secondType:
[ { _id: '545d533e2c21b900000ad236', type: 1, pushId: '125' },
{ _id: '545d533e2c21b900000ad237', type: 1, pushId: '126' } ] }

Resources