Mongo DB complex query - mongodb

I need some help with MongoDB.
I Have Ex Schema and Database as below:
`
Tb:
User:
{
name: { type: String }
image: { type: String }
city: { type: String }
address: [{
index: { type: Number }
district_id: { type: Schema.ObjectId, ref: 'District' }
}]
}
Tb2:
District:{
name: { type: String }
code: { type: String }}
Example Database:
User: [{
_id: '1234'
name: 'Jacky',
image: '',
city: 'Da Nang',
address: [
{
index: 1,
district_id: '12345'
},
{
index: 2,
district_id: '123456'
},
{
index: 3,
district_id: '1234567'
}
]
}]
District: [
{
_id: '12345',
name: 'Hai Chau',
code: 12
},
{
_id: '123455',
name: 'Lien CHieu',
code: 13
},
{
_id: '1234567',
name: 'Cam Le',
code: 14
},
{
_id: '12345678',
name: 'Son Tra',
code: 15
}
]
How can i select User by both of two options (disctrict.name && index) like as district.name = 'Lien Chieu' && address.index > 1.

You can try this below query :
db.District.aggregate([
/** filter required doc from district Coll */
{ $match: { name: "Lien CHieu" } },
{
$lookup: {
from: "User",
let: { id: "$_id" },
pipeline: [
/** Basic filter to get matched doc from User Coll */
{ $match: { $expr: { $in: ["$$id", "$address.district_id"] } } },
/** check for criteria */
{
$addFields: {
mappingField: {
$map: {
input: "$address", as: "each", in: {
$and: [{ $gt: ["$$each.index", 1] }, { $eq: ["$$id", "$$each.district_id"] }]
}
}
}
}
},
/** filter required doc from above which matches our criteria & remove added field */
{ $match: { mappingField: true } }, { $project: { mappingField: 0 } }
],
as: "userdata"
}
}
])
Test : MongoDB-Playground

Related

Does MongoDB support optional filters?

I have a case where I want to run a query that MUST match some field conditions but SHOULD match some others. If they don’t match however the query should still return the conditions that MUST.
For example let’s say in a collection I have 3 documents such as:
{ _id: 1, position: “Developer”, name: “Greg”, surname: “Smith” },
{ _id: 2, position: “QA”, name: “Andrew”, surname: “Samson” },
{ _id: 3, position: “Developer”, name: “Adam”, surname: “Mount” }
If I run a query with a condition { position: “Developer” } that is a MUST and { name: “Greg” } that is a SHOULD I should just get the record:
{ _id: 1, position: “Developer”, name: “Greg”, surname: “Smith” }
However, if I run the query again with { position: “Developer” } and { “name”: “Daniel” }, I should get all records that match the MUST condition, therefore I don't want the position condition to fail the query if it doesn't match. So return records:
{ _id: 1, position: “Developer”, name: “Greg”, surname: “Smith” },
{ _id: 3, position: “Developer”, name: “Adam”, surname: “Mount” }
Furthermore, if I have a query with MUST conditon { position: “Developer” } and SHOULD conditions { name: “Greg”, surname: “Garbrandt” } I should still get:
{ _id: 1, position: “Developer”, name: “Greg”, surname: “Smith” }
Not sure if there is a way to write the query to work like this or if there is a functionality that could do this outright.
You can use this one:
var name = "Daniel";
db.collection.aggregate([
{ $match: { position: "Developer" } },
{
$match: {
$expr: {
$cond: {
if: db.collection.findOne({ position: "Developer", name: name }),
then: { $eq: ["$name", name] },
else: true
}
}
}
}
])
Or this one:
db.collection.aggregate([
{ $match: { position: "Developer" } },
{ $group: { _id: null, data: { $push: "$$ROOT" } } },
{ $set: { cond: { $filter: { input: "$data", cond: { $eq: ["$$this.name", "Daniel"] } } } } },
{
$project: {
data: {
$cond: {
if: { $eq: ["$cond", []] },
then: "$data",
else: "$cond"
}
}
}
},
{ $unwind: "$data" },
{ $replaceWith: "$data" }
])

Aggregate $lookup Array of Objects

I have collection schools with field groups and I am attempting to $lookup documents from the users collection. I am getting empty results however and an extra schools document.
Schools Schema
const SchoolSchema = new Schema({
groups: [
{
name: { type: String },
color: { type: String },
userDrivenName: { type: String },
},
]
});
module.exports = School = mongoose.model("School", SchoolSchema);
User Schema
const UserSchema = new Schema({
name: {
type: String,
required: true,
},
groups: [
{
groupId: { type: String },
name: { type: String },
color: { type: String },
userDrivenName: { type: String },
},
]
});
Query
db.schools.aggregate([
{
$match: {
_id: ObjectId("5d836e584a24e20e6090fd7b")
}
},
{
$project: {
groups: 1
}
},
{
$unwind: "$groups"
},
{
$lookup: {
from: "users",
let: {
groupId: "$groups._id"
},
pipeline: [
{
$match: {
"groups.groupId": "$$groupId"
}
}
],
as: "groups",
},
},
])
Results:
[
{
"_id": "5d836e584a24e20e6090fd7b",
"groups": []
},
{
"_id": "5d836e584a24e20e6090fd7b",
"groups": []
}
]
Expected Results:
[
{
"_id":"5d836e584a24e20e6090fd7b",
"groups":[
{
"_id":"5ec01fdc1dfb0a4f08316dfe",
"name":"GROUP 1",
"users":[
{
"name":"Luke Skywalker"
}
]
}
]
}
]
MongoPlayground
Two things:
There's a type mismatch between groupId and groups.groupId so you need to use $toString (based on your Mongo Playground example),
$lookup with custom pipelines allows only expression when you use $match so you need $in and $expr:
{
$lookup: {
from: "users",
let: { groupId: { $toString: "$groups._id" } },
pipeline: [
{
$match: {
$expr: {
$in: ["$$groupId","$groups.groupId"]
}
}
}
],
as: "groups"
}
}
Mongo Playground

Returning unique mongodb documents using distinct not working

Each message document has a messageTrackingId. I want to return all the messages, but exclude documents that have the same messageTrackingId. So for example, if I had 4 documents in my table and 3 of them have the same messageTrackingId value, then the Messages.find() should only return 2 documents.
I'm trying to use distinct to only return the unique documents so I don't get duplicates with the same messageTrackingId. Currently postman is returning no documents.
if I changed
Messages.find({query}).distinct('messageTrackingId')
to
Messages.find(query)
then it returns all the recipientId's documents. but when I add distinct, I get no results.
app.get('/api/messages',(req, res, next)=>{
query = {};
inbox = false;
messageId = false;
if(req.query.recipientId){
query = { recipientId: req.query.recipientId }
inbox = true;
Messages.aggregate(// Pipeline
[
// Stage 1
{
$group: {
_id: "$messageTrackingId",
message : { $addToSet: '$message' },
recipientId : { $addToSet: '$recipientId' },
creator : { $addToSet: '$creator' },
messageTrackingId : { $addToSet: '$messageTrackingId' },
}
},
// Stage 2
{
$project: {
_id: 1,
message: { $arrayElemAt: ["$message", 0 ] },
recipientId: { $arrayElemAt: ["$recipientId", 0 ] },
creator: { $arrayElemAt: ["$creator", 0 ] },
messageTrackingId: { $arrayElemAt: ["$messageTrackingId", 0 ] }
}
}
])
messages model
const mongoose = require("mongoose");
const uniqueValidator = require("mongoose-unique-validator");
const messagingSchema = mongoose.Schema({
creator: { type: mongoose.Schema.Types.ObjectId, ref: "User" },
recipient: { type: String, required: true },
recipientId: { type: String, required: true },
message: { type: String, required: true },
subject: { type: String, required: true },
creationDate: { type: Date, required: true },
messageTrackingId: { type: String }
// readDate: { type: Date, required: true }
});
module.exports = mongoose.model("Messages", messagingSchema);
distinct will return distinct fields which is not what you want.
You will need to use aggregation and group by the messageTrackingId, then project grabbing the first message content etc you want:
Given sample data like:
{ "messageTrackingId" : 1, "message" : "hello" }
{ "messageTrackingId" : 1, "message" : "hello" }
{ "messageTrackingId" : 1, "message" : "bye" }
{ "messageTrackingId" : 2, "message" : "bye" }
{ "messageTrackingId" : 2, "message" : "bye" }
{ "messageTrackingId" : 1, "message" : "hello" }
In MongoDB:
db.getCollection("messages").aggregate(
// Pipeline
[
// Stage 1
{
$group: {
_id: "$messageTrackingId",
message : { $addToSet: '$message' }
}
},
// Stage 2
{
$project: {
_id: 1,
message: { $arrayElemAt: ["$message", 0 ] }
}
},
]);
To use in mongoose, simply using the aggregate function on your model:
Using Mongoose
const result = await Message.aggregate(// Pipeline
[
// Stage 1
{
$group: {
_id: "$messageTrackingId",
message : { $addToSet: '$message' }
}
},
// Stage 2
{
$project: {
_id: 1,
message: { $arrayElemAt: ["$message", 0 ] }
}
},
]);
UPDATE AFTER COMMENTS
If you need to query for a given messageTrackingId, then add $match as the first stage of the pipeline:
const result = await Message.aggregate(
[
{
$match: {
messageTrackingId: {$eq: 2}
}
},
{
$group: {
_id: "$messageTrackingId",
message : { $addToSet: '$message' }
}
},
{
$project: {
_id: 1,
message: { $arrayElemAt: ["$message", 0 ] }
}
},
]);

Do an aggregate with a populate

I'm having troubles with the following. I wonder if it's possible to do it with a single query.
So I have the following model :
const Analytics = new Schema({
createdAt: {
type: Date,
default: Moment(new Date()).format('YYYY-MM-DD')
},
loginTrack: [
{
user_id: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Users',
}
}
]
}, { collection: 'analytics' });
And the user model :
const UserSchema = new mongoose.Schema(
{
nickname: {
type: String,
required: true,
unique: true
},
instance: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Instances',
default: null
}}, {collection: 'users'});
I want to get the connected users for a specific instance at a specific date.
AnalyticsModel.aggregate([
{
$match: {
createdAt: { "$gte": moment(args.startDate).format('YYYY-MM-DD'), "$lt": moment(args.endDate).format('YYYY-MM-DD')}
}
},
{
"$project": {
users: { $size: "$loginTrack" },
"createdAt": 1,
"_id": 0
}
}, {
"$group": {
"_id": "$createdAt",
"count": { "$sum": "$users" }
}
}
This gets me
[ { _id: '2019-02-11', count: 3 },
{ _id: '2019-02-08', count: 6 },
{ _id: '2019-02-07', count: 19 },
{ _id: '2019-02-06', count: 16 } ]
The results expected will be the same but I want to filter on users that belongs to a specific instance
Is it possible to do it with a single query or I need to do a populate first before the aggregation ?
UPDATE
I did some progress on it, I needed to add a lookup and I think it's ok :
AnalyticsModel.aggregate([
{"$unwind": "$loginTrack"},
{
$lookup:
{
from: 'users',
localField:'loginTrack.user_id',
foreignField: '_id',
as: '_users'
}
},
{
$match: {
createdAt: { "$gte": new Date(args.startDate), "$lt": new Date(args.endDate)}
}
},
{
$project: {
_users: {
$filter: {
input: '$_users',
as: 'item',
cond: {
$and: [
{ $eq: ["$$item.instance", new ObjectId(args.instance_id)] }
]
}
}
},
"createdAt": 1,
"_id": 0
}
},
{
"$group": {
"_id": "$createdAt",
"count": { "$sum": { "$size": "$_users" } }
}
}
Also the dates were in string in the model.
The output is now :
[ { _id: 2019-02-11T00:00:00.000Z, count: 2 } ]

Check follow status in aggregate mongo / mongoose

I have this schema for users where followers/followed is array and the reference the same schema
var userSchema = new Schema({
username: { type: String, unique: true, trim: true, required: true },
password: { type: String, required: true },
followers: [{ type: Schema.Types.ObjectId, ref: "users" }],
followed: [{ type: Schema.Types.ObjectId, ref: "users" }],
registered: { type: Date, default: Date.now },
admin: { type: Number, default: 0 }
});
What I am looking for to return the follow status, if the _id is contains in followed array give me for example follow_status: 1
[
{
$match: { username: new RegExp(username, "i") }
},
{
$unwind: "$followers"
},
{
$lookup: {
from: "users",
localField: "followers",
foreignField: "_id",
as: "info"
}
},
{
$unwind: "$info"
},
{
$project: {
info: {
_id: 1,
username: 1,
avatar: { $ifNull: ["$avatar", ""] },
fullname: { $ifNull: ["$fullname", ""] }
}
}
},
{
$replaceRoot: { newRoot: "$info" }
},
{
$limit: 1000
}
]
Current pipeliens result
[
{
"_id": "5a906653f52e66c9c7a23cb6",
"username": "User1"
},
{
"_id": "5a908eb564a726cf8ec7e0a3",
"username": "User2"
}
]