Applying $exists to MongoDB aggregation - mongodb

I have two mongo collections structured like so:
customers
{
_id: ObjectId,
name: String,
companyId: ObjectId
}
companies
{
_id: ObjectId,
name: String,
rights: [
add: boolean,
edit: boolean,
shop: boolean
]
}
So each customer has a companyId that lets us look up the companies.rights available. I need to get a list of which companies have customers but don't have the shop property at all.
So far I have this:
db.getCollection('customers').aggregate([
{
$match: {}
},
{
$lookup: {
from: 'companies',
localField: 'companyId',
foreignField: '_id',
as: 'company'
}
},
{
$project: {
companyId: '$company._id',
companyName: '$company.name',
shopSetting: '$company.rights.shop'
}
}
])
This seems to be working ok to give me all of the companies with their shop value (true or false). But what if I only want to see the companies that don't have the shop field existing at all? How would I modify this query to accomplish that? I've tried reading up on the $exists field in mongo, but this is all pretty new to me so I'm not sure where to apply it here.
Note: I need to query the companies from the customers collection because there are some companies without customers and I need this result to only be companies that are assigned to customers but don't have the rights.shop property existing

db.customers.aggregate([
{ $match: {} },
{
$lookup: {
from: "companies",
localField: "companyId",
foreignField: "_id",
as: "company",
pipeline: [
{
$match: {
$expr: {
$eq: [
{
$filter: {
input: "$rights",
as: "r",
cond: "$$r.shop"
}
},
[]
]
}
}
}
]
}
},
{
$project: {
companyId: "$company._id",
companyName: "$company.name",
shopSetting: "$company.rights.shop"
}
}
])
mongoplayground

Related

$lookup with pipeline match and projection does not work for guid

I have two collections that I want to join with $lookup based on two id fields. Both fields are from type guid and looke like this in mongodb compass: 'Binary('cavTZa/U2kqfHtf08sI+Fg==', 3)'
This syntax in the compass aggregation pipeline builder gives the expected result:
{
from: 'clients',
localField: 'ClientId',
foreignField: '_id',
as: 'ClientData'
}
But i want to add some projection and tried to change it like this:
{
from: 'clients',
'let': {
id: '$_id.clients'
},
pipeline: [
{
$match: {
$expr: {
$eq: [
'$ClientId',
'$$id'
]
}
}
},
{
$project: {
Name: 1,
_id: 0
}
}
],
as: 'ClientData'
}
But the result here is that every client from collection 'clients' is added to every document in the starting table. I have to use MongoDB 3.6 so the new lookup syntax from >=5.0 is not available.
Any ideas for me? Does $eq work for binary stored guid data?
In the first example, you say that the local field is ClientId and the foreign field is _id. But that's not what you used in your second example.
This should work better:
{
from: 'clients',
'let': {
ClientId: '$ClientId'
},
pipeline: [
{
$match: {
$expr: {
$eq: [
'$$ClientId',
'$_id'
]
}
}
},
{
$project: {
Name: 1,
_id: 0
}
}
],
as: 'ClientData'
}

Best way to retrieve a reference field using $lookup in Mongoose?

I have two models Book and Author, Book has reference of Author, suppose an Author got deleted then I only want to retrieve those Books who have an author:
BookSchema with these fields
name: String,
author: {
type: Schema.Types.ObjectId,
ref: 'Author',
required: [true, 'A book must have an author']
}
AuthorSchema with these fields
name: String
I have to do it using $lookup operator. I am able to get the desired result but I don't know if it's a good way. This is my solution:
const books = await Book.aggregate([
{ $lookup: {
from: 'authors',
localField: 'author',
foreignField: '_id',
as: 'bookAuthor'
}
},
{ $match: { bookAuthor: { $not: { $size: 0 } } } },
{ $unwind: '$bookAuthor' },
{ $project: {
name: 1,
bookAuthor: { name: 1 }
}
}
]);
You have done everything correct, but you don't need to use the $match in second phase of aggregation pipeline. $unwind will automatically remove the documents with empty bookAuthor array, so if there is no author, it will be removed after $unwind stage.
Try this:
const books = await Book.aggregate([
{ $lookup: {
from: 'authors',
localField: 'author',
foreignField: '_id',
as: 'bookAuthor'
}
},
{ $unwind: '$bookAuthor' },
{ $project: {
name: 1,
bookAuthor: { name: 1 }
}
}
]);
Have a look at this Mongo Playground to see it working

Applying condition on localfield in aggregate in mongodb

I have two collections user and transaction, transaction has two fields customer and seller , and user has two fields name and email., user collection contains data of customers and sellers
I want output as if the email passed belongs to customer then seller details should come from lookup and if email passed belongs to seller then customer details should come from lookup
What I have done is, I am passing the value email dynamically , email value can belongs to customer or seller , If email matches seller then I want the localfield in lookup as customer and vice versa. Following is what I have tried.
const email = req.email;
transaction.aggregate([{
$match: {
$or: [{
customer: email,
},
{
seller: email,
},
],
},
},
{
$lookup: {
from: "user",
localField: {
$cond: [{
if: {
$eq: ["$customer", email]
},
then: "seller",
else: "customer",
}],
},
foreignField: "email",
as: "user",
},
},
{
$unwind: "$user"
},
]);
But for the above query error comes as below
$lookup argument 'localField' must be a string
I am using nodejs, express and mongoose.
The localField allows only string, you can use lookup with pipeline,
let to define your conditions logic
pipeline to put your matching condition with lookup collection's field
{
$lookup: {
from: "user",
let: {
localField: {
$cond: [{
if: { $eq: ["$customer", email] },
then: "$seller",
else: "$customer",
}]
}
},
pipeline: [
{ $match: { $expr: { $eq: ["$email", "$$localField"] } } }
],
as: "user"
}
}

MongoDB lookup with object relation instead of array

I have a collection matches like this. I'm using players object {key: ObjectId, key: ObjectID} instead of classic array [ObjectId, ObjectID] for reference players collection
{
"_id": ObjectId("5eb93f8efd259cd7fbf49d55"),
"date": "01/01/2020",
"players": {
"home": ObjectId("5eb93f8efd259cd7fbf49d59"),
"away": ObjectId("5eb93f8efd259cd7fbf49d60")
}
},
{...}
And players collection:
{
"_id": ObjectId("5eb93f8efd259cd7fbf49d59"),
"name": "Roger Federer"
"country": "Suiza"
},
{
"_id": ObjectId("5eb93f8efd259cd7fbf49d60"),
"name": "Rafa Nadal"
"country": "España"
},
{...}
What's the better way to do mongoDB lookup? something like this is correct?
const rows = await db.collection('matches').aggregate([
{
$lookup: {
from: "players",
localField: "players.home",
foreignField: "_id",
as: "players.home"
}
},
{
$lookup: {
from: "players",
localField: "players.away",
foreignField: "_id",
as: "players.away"
},
{ $unwind: "$players.home" },
{ $unwind: "$players.away" },
}]).toArray()
I want output like this:
{
_id: 5eb93f8efd259cd7fbf49d55,
date: "12/05/20",
players: {
home: {
_id: 5eb93f8efd259cd7fbf49d59,
name: "Roger Federer",
country: "Suiza"
},
away: {
_id: 5eb93f8efd259cd7fbf49d60,
name: "Rafa Nadal",
country: "España"
}
}
}
{...}
You can try below aggregation query :
db.matches.aggregate([
{
$lookup: {
from: "players",
localField: "players.home",
foreignField: "_id",
as: "home"
}
},
{
$lookup: {
from: "players",
localField: "players.away",
foreignField: "_id",
as: "away"
}
},
/** Check output of lookup is not empty array `[]` & get first doc & write it to respective field, else write the same value as original */
{
$project: {
date: 1,
"players.home": { $cond: [ { $eq: [ "$home", [] ] }, "$players.home", { $arrayElemAt: [ "$home", 0 ] } ] },
"players.away": { $cond: [ { $eq: [ "$away", [] ] }, "$players.away", { $arrayElemAt: [ "$away", 0 ] } ] }
}
}
])
Test : mongoplayground
Changes or Issues with current Query :
1) As you're using two $unwind stages one after the other, If anyone of the field either home or away doesn't have a matching document in players collection then in the result you don't even get actual match document also, But why ? It's because if you do $unwind on [] (which is returned by lookup stage) then unwind will remove that parent document from result, To overcome this you need to use preservenullandemptyarrays option in unwind stage.
2) Ok, there is another way to do this without actually using $unwind. So do not use as: "players.home" or as: "players.away" cause you're actually writing back to original field, Just in case if you don't find a matching document an empty array [] will be written to actual fields either to "home" or "away" wherever there is not match (In this case you would loose actual ObjectId() value existing in that particular field in matches doc). So write output of lookup to a new field.
Or even more efficient way, instead of two $lookup stages (Cause each lookup has to go through docs of players collection again & again), you can try one lookup with multiple-join-conditions-with-lookup :
db.matches.aggregate([
{
$lookup: {
from: "players",
let: { home: "$players.home", away: "$players.away" },
pipeline: [
{
$match: { $expr: { $or: [ { $eq: [ "$_id", "$$home" ] }, { $eq: [ "$_id", "$$away" ] } ] } }
}
],
as: "data"
}
}
])
Test : mongoplayground
Note : Here all the matching docs from players which match with irrespective of away or home field will be pushed to data array. So to keep DB operation simple you can get that array from DB along with actual matches document & Offload some work to code which is to map respective objects from data array to players.home & players.away fields.

$lookup using multiple criteria mongodb java aggregation

Have 2 following collections:
user collection
{
userId:user1,
creationTimeStamp:2019-11-05T08:15:30
status:active
},
{
userId:user2,
creationTimeStamp:2019-10-05T08:15:30
status:active
}
document collection
{
userId:user1,
category:Development
published:true
},
{
userId:user2,
category:Development
published:false
}
I want to join these two collections and filter users such that documents which are of development category and are not published from active users between creationtimestamp
How can I write a mongodb java aggregation in order to get a result like this:
{
userId: user2,
status:active,
category:Development,
published:false
}
You could run below aggregation query on the document collection to get the expected result
[{$match: {
category:'development',
published: false
}}, {$lookup: {
from: 'user',
localField: 'userId',
foreignField: 'userId',
as: 'JoinedTable'
}}, {$unwind: {
path: '$JoinedTable'
}}, {$group: {
_id: '$_id',
userId: {
$first: '$userId'
},
status: {
$first: '$JoinedTable.status'
},
category: {
$first: '$category'
},
published: {
$first: '$published'
},
}}]
Explanation:
1. filter documents using match for criteria category: 'development' & published: false
2. join document collection with user collection with key userId
3. unwind the joined collection field to convert array to object
4. project the fields needed using groups.
Hope this helps!
You haven't mentioned about the duplicate of userId in User collection.
So the script is
[{
$match: {
category: "Development",
published: false
}
}, {
$lookup: {
from: 'user',
localField: 'userId',
foreignField: 'userId',
as: 'joinUser'
}
}, {
$unwind: {
path: "$joinUser",
preserveNullAndEmptyArrays: true
}
}, {
$match: {
"joinUser.status": "active"
}
}, {
$addFields: {
"status": "$joinUser.status"
}
}, {
$project: {
_id: 0,
userId: 1,
category: 1,
published: 1,
status: 1
}
}]
And the java code,
include these imports
import static org.springframework.data.mongodb.core.aggregation.Aggregation.match;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.lookup;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.unwind;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.project;
method is,
public Object findAllwithVideos() {
Aggregation aggregation=Aggregation.newAggregation(
match(Criteria.where("category").is("Development").and("published").is(false)),
lookup("user","userId","userId","joinUser"),
unwind("joinUser",true),
new AggregationOperation(){
#Override
public Document toDocument(AggregationOperationContext aggregationOperationContext){
return new Document("$addFields",
new Document("status","$joinUser.status")
);
}
},
project("userId","category","published","status")
).withOptions(AggregationOptions.builder().allowDiskUse(Boolean.TRUE).build());
return mongoTemplate.aggregate(aggregation, mongoTemplate.getCollectionName(Document.class), Object.class);
}