MongoDB - how to use $ifNull only on a field that exists - javascript

I'm using $lookup, which may or may not return something. But if it does, i need to check the looked up value and check a specific field to see if it is null, and if it is, set it to something.
I tried this:
{
$lookup:
{
from:
"item_templates",
localField:
"based_on",
foreignField:
"_id",
as:
"template"
}
},
{
$addFields:
{
"template.image.url":
{
$ifNull:
[
"$template.image.url",
"http://via.placeholder.com/400x700/d3d3d3/000000/?text=No%20Image&"
]
}
}
},
but it seems to have no effect on template.image.url (which is an array of objects), it actually returns an array instead, with one element as null.

You can use newer $lookup syntax to achieve the expected result
db.collection.aggregate([
{ "$lookup": {
"from": "item_templates",
"let": { "based_on": "$based_on" },
"pipeline": [
{ "$match": { "$expr": { "$eq": ["$_id", "$$based_on"] }}},
{ "$addFields": {
"image.url": {
"$ifNull": [
"$image.url",
"http://via.placeholder.com/400x700/d3d3d3/000000/?text=No%20Image&"
]
}
}}
],
"as": "template"
}}
])

Try with $project.
{
$unwind:{
path: "$template",
includeArrayIndex: "arrayIndex",
preserveNullAndEmptyArrays: true
}
},
{
$project:{
template.image.url: {
$ifNull: [
"$template.image.url",
"http://via.placeholder.com/400x700/d3d3d3/000000/?text=No%20Image&"
]
},
}
}

Related

Updating Mongodb docs with an array of objects?

Suppose we have this array:
const array = [{ code:1, pw:'abc'}, { code:2, pw:'grt'}, { code:3, pw:'tpo'}, { code:4, pw:'xyz'}]
and we have these docs in our db from model called User:
[{ code:1, pw:'___'}, { code:2, pw:'___'}, { code:3, pw:'___'}, { code:4, pw:'___'}]
What's the most efficient way you'd suggest to update the pw fields from db with pws from the array at one shot (in Mongoose)? (we definitely want the codes from both arrays to match) Thank you.
A simple and efficient option will be to use a bulk:
const usersBulk = userModel.collection.initializeUnorderedBulkOp();
for (const user of array) {
usersBulk.find({code: user.code}).update({$set: {pw: user.pw}});
}
usersBulk.execute()
It can also be done in an update with pipeline query:
db.collection.updateMany(
{code: {$in: codes}},
[
{$set: {pw: {
$getField: {
field: "pw",
input: {
$first: {
$filter: {
input: array,
cond: {$eq: ["$$this.code", "$code"]}
}
}
}
}
}}}
]
)
See how it works on the playground example
But I think it might be less efficient than a bulk update.
You can do it like this:
db.collection.update({
"code": {
"$in": [
1,
2,
3,
4
]
}
},
[
{
"$set": {
"pw": {
"$cond": {
"if": {
"$eq": [
"$code",
1
]
},
"then": "abc",
"else": {
"$cond": {
"if": {
"$eq": [
"$code",
2
]
},
"then": "grt",
"else": {
"$cond": {
"if": {
"$eq": [
"$code",
3
]
},
"then": "tpo",
"else": "xyz"
}
}
}
}
}
}
}
}
],
{
multi: true
})
Working example

mongoose nested array filter

I have a document form similar to this
{
"doc-id":2,
"interfaces": [
{
"interface-role": "ON",
"port-nb": 1
},
{
"interface-role": "OFF",
"port-nb": 2
},
{
"interface-role": "ON",
"port-nb": 3
},
{
"interface-role": "OFF",
"port-nb": 3
}
]
}
I want to query and get specific document interfaces and also have the ability to filter ON and OFF and that's what I did try so far
const doc = await this.doc
.findOne({
'doc-id': docId,
'interfaces["interface-role"]': interfaceRole, //ON or OFF
})
.select({ interfaces: 1, _id: 0 })
.exec();
so the result that I want to have is getting interfaces if there's no filter for interfaces-role and if there's one get the interfaces filtered
You can use a $or to do the conditional filtering with a $filter.
db.collection.aggregate([
{
$match: {
"doc-id": 2
}
},
{
"$addFields": {
"interfaces": {
"$filter": {
"input": "$interfaces",
"as": "i",
"cond": {
$or: [
{
$eq: [
null,
<interfaceRole>
]
},
{
$eq: [
"$$i.interface-role",
<interfaceRole>
]
}
]
}
}
}
}
}
])
Here is the Mongo playground when interfaceRole is not supplied.
Here is the Mongo playground when interfaceRole is supplied.

Set lookup variable to be the value of an expression

This is the sample of my noteCollection's document
{
title: "Some title",
body: "Descreptive body",
contributedBy: [
{
contributorId: ObjectId(5353ff535f5f3a5)
}
] // for some reason, this field has to be an array of objects
}
Now, while using $lookup to "lookup" for the contributor, I need the contributorId field
The problem is, I don't know how to express contributedBy[0].contributorId in MongoDB query way.
// lookup aggregation
{
from: 'contributors',
as: 'contributor',
let: {'contributorUserId': '$contributedBy[0].contributorId'}, // here, how to get the value of `contributorId` property of the 0th element in `contributedBy` field
pipeline: [
{$match: {
$expr: {$eq: ['$_id', '$$contributorUserId']}
}}
]
}
If you want to join those two collections by only one contributedBy.contributorId at 0th index, use $arrayElemAt operator:
db.noteCollection.aggregate([
{
$lookup: {
from: 'contributors',
as: 'contributor',
let: {
'contributorUserId': {
$arrayElemAt: ['$contributedBy.contributorId', 0]
}
},
pipeline: [
{
$match: {
$expr: { $eq: ['$_id', '$$contributorUserId'] }
}
}
]
}
}
])
If you want to join the two collections w.r.t to their reference _ids then use $in operator:
db.noteCollection.aggregate([
{
$lookup: {
from: 'contributors',
as: 'contributor',
let: {
'contributorUserIds': '$contributedBy.contributorId'
},
pipeline: [
{
$match: {
$expr: { $in: ['$_id', '$$contributorUserIds'] }
}
}
]
}
}
])

MongoDB aggregation lookup additional field from localField to result

I have the following collections in MongoDB (mongoplayground) [characters,guilds]
I want to make a $lookup, that will add rank field to the resulting document, like that:
"members_t": [
{
"_id": ObjectId("5a934e000102030405000000"),
"level": 20,
"name": "test1",
"rank": 1
},
{
"_id": ObjectId("5a934e000102030405000001"),
"level": 40,
"name": "test2",
"rank": 2
}
]
old $lookup syntax can't help me with that, but the following query with the new syntax returns me an empty array in tested field (even without $addFields stage):
{
$lookup: {
from: "characters",
let: {
members_name: "$members.name",
rank: "$members.rank"
},
pipeline: [
{
$match: {
name: "$$members_name"
}
}
],
as: "tested"
}
}
So, is there any option to add an additional field after $lookup stage or not?
(Mongo -v 4.2.3, so the problem is not related with new syntax support)
You were almost close to solving it. Since the members are an array, you need to pass it through $lookup and then conditionally join it per each character with $reduce (You can do this with $filter as well, but then we need aditional stages).
Note: Explanation why we need to use $expr inside $lookup pipeline.
Try this one:
db.guilds.aggregate([
{
$lookup: {
from: "characters",
let: {
members: "$members"
},
pipeline: [
{
$match: {
$expr: {
$in: [
"$name",
"$$members.name"
]
}
}
},
{
$addFields: {
rank: {
$reduce: {
input: "$$members",
initialValue: null,
in: {
$cond: [
{
$eq: [
"$$this.name",
"$name"
]
},
"$$this.rank",
"$$value"
]
}
}
}
}
}
],
as: "members_t"
}
}
])
MongoPlayground

convert a $lookup result to an object instead of array

I'm doing a $lookup from a _id. So the result is always 1 document. Hence, I want the result to be an object instead an array with one item.
let query = mongoose.model('Discipline').aggregate([
{
$match: {
project: mongoose.Types.ObjectId(req.params.projectId)
},
},
{
$lookup: {
from: "typecategories",
localField: "typeCategory",
foreignField: "_id",
as: "typeCategory"
}
},
{
$project: {
title: 1, typeCategory: "$typeCategory[0]"
}
}
]);
This notation: "$typeCategory[0]" is not working. Is there any smart way of doing this?
You can just use $unwind. It deconstructs an array field from the input documents to output a document for each element
let query = mongoose.model('Discipline').aggregate([
{
$match: {
project: mongoose.Types.ObjectId(req.params.projectId)
},
},
{
$lookup: {
from: "typecategories",
localField: "typeCategory",
foreignField: "_id",
as: "typeCategory"
}
},
{$unwind: '$typeCategory'},
{
$project: {
title: 1, typeCategory: "$typeCategory"
}
}
]);
You can use $arrayElemAt in $project stage.
Syntax of $arrayElemAt is { $arrayElemAt: [ <array>, <idxexOfArray> ] }
like:
mongoose.model('Discipline').aggregate([
{
$match: {
project: mongoose.Types.ObjectId(req.params.projectId)
},
},
{
$lookup: {
from: "typecategories",
localField: "typeCategory",
foreignField: "_id",
as: "typeCategory"
}
},
{
$project: {
name: 1, typeCategory: {$arrayElemAt:["$typeCategory",0]}
}
}
]);
Use $first to return the first element in the array:
$project: {
title: 1,
typeCategory: {$first: "$typeCategory"}
}
For merging two collections:
{$replaceRoot: { newRoot: { $mergeObjects: [ { $arrayElemAt: [ "$typeCategory", 0 ] }, "$$ROOT" ] } } },
{ $project: { typeCategory: 0 } }

Categories