MongoDB conditionial update - mongodb

I am running a bit in circles here and would appreciate some help. What I am looking to do is either update or create a nested object contained in an array depending on whether this object exists.
I have a users collection and a user document has the following structure:
{
schema_version: 1,
display_name: 'xxxxxx',
email: 'xxxxxx',
email_verified:'xxxxxx',
...
custom_data: {
stripe_id: 'xxx',
subscriptions: [{
subscription_id: xxxx,
....
}],
...
},
}
In webhook calls from Stripe I am getting a subscription object with a subscription_id and a stripe_id.
What I want to do is check if subscription_id exists, if so, update the document, if not then create the document in the subscriptions array for the user document where stripe_id matches.
If I do something along the lines of:
db.collection.update(
{subscription_id: subscription.id},
{ $set: { 'custom_data.subscriptions': subscriptionData } },
{ upsert: true }
)
The problem is that I am creating subscription objects not bound to my user document where stripeID matches.
On the other hand, if I do something like this:
db.collection.update(
{'custom_data.stripe_id': stripe_id},
{ $set: { 'custom_data.subscriptions': subscriptionData } },
{ upsert: true }
)
I will potentially end up creating dupes in the subscriptions array when, in fact I would want to update the existing object where subscription_id matches.
Is there any way to do that in one query with Mongo, or will I have to resort to using 2 queries in an if statement?
Thanks in advance for any clarification on this.

You can do the followings with an aggregation pipeline:
$match with $or condition to search for custom_data.subscriptions.subscription_id or custom_data.stripe_id
$addFields with $map to conditional update your subscription object when matched
$addFields with $setUnion to insert an entry of incoming subscription object for the insert case
$merge to update the back into the original collection
db.collection.aggregate([
{
$match: {
$expr: {
$or: [
{
$eq: [
"$custom_data.subscriptions.subscription_id",
"xxxx"
]
},
{
$eq: [
"$custom_data.stripe_id",
"xxx"
]
}
]
}
}
},
{
"$addFields": {
"custom_data": {
subscriptions: {
"$map": {
"input": "$custom_data.subscriptions",
"as": "s",
"in": {
"$cond": {
// if subscription_id matched, replace with your incoming object
"if": {
$eq: [
"$$s.subscription_id",
"xxxx"
]
},
"then": {
subscription_id: "incoming_sub_id"
},
// if not matched, keep the original object
"else": "$$s"
}
}
}
}
}
}
},
{
"$addFields": {
"custom_data": {
subscriptions: {
// insert case; if the subscription array is empty, then union with your incoming object
$setUnion: [
"$custom_data.subscriptions",
[
{
subscription_id: "incoming_sub_id"
}
]
]
}
}
}
},
{
"$merge": {
"into": "collection",
"on": "_id",
"whenMatched": "replace"
}
}
])
Here is the Mongo playground for your reference.

Related

A MongoDB update query to update array values to their lowercase value

I have a Mongo collection of states, where each state contains an array of cities:
{
"_id":"636d1137cf1e57408486f795",
"state":"new york",
"cities":[
{
"cityid":"62bd8fa5396ba8aef4ad1041",
"name":"Yonkers"
},
{
"cityid":"62bd8fa5396ba8aef4ad1043",
"name":"Syracuse"
}
]
}
I need an update query that will lowercase every cities.name in the collection.
I can do an update with a literal value e.g.
db.states.updateMany(
{},
{ $set: { "cities.$[].name" : "some_value" } }
)
... , but I need the value to be based on the existing value. The closest I can get is something like this (but that doesn't work -- FieldPath field names may not start with '$')
db.states.updateMany(
{},
{ $set: { "cities.$[].name" : { $toLower: "cities.$[].name"} } }
)
You can chain up $map and $mergeObjects to perform the update. Put it in an aggregation pipeline in update.
db.collection.update({},
[
{
$set: {
cities: {
"$map": {
"input": "$cities",
"as": "c",
"in": {
"$mergeObjects": [
"$$c",
{
"name": {
"$toLower": "$$c.name"
}
}
]
}
}
}
}
}
])
Mongo Playground

How can I $unset a sub-document that's part of another sub-document in MongoDB?

I have a collection containing documents of the following form:
{
tokens: {
name: {
value: "...",
},
...
}
}
name can be anything, and there can be multiple embedded documents assigned to different keys, all of which have a value field.
How do I $unset all embedded documents that have a certain value? { $unset: { "tokens.value": "test" } } doesn't work, and nor does "tokens.$[].value" or "tokens.$.value".
You can use pipelined form of update, like this:
db.collection.update({},
[
{
"$set": {
"tokens": {
$arrayToObject: {
"$filter": {
"input": {
"$objectToArray": "$$ROOT.tokens"
},
"as": "item",
"cond": {
"$ne": [
"$$item.v.value",
"test"
]
}
}
}
}
}
}
],
{
multi: true
})
Playground link.
In this query, we do the following:
We convert tokens object to an array, using $objectToArray.
We filter out the array elements which have a value key present in embedded documents and have a value of test using $filter.
Finally we convert the filtered array back again to object, and assign it to tokens key.

Conditionally update/upsert embedded array with findOneAndUpdate in MongoDB

I have a collection in the following format:
[
{
"postId": ObjectId("62dffd0acb17483cf015375f"),
"userId": ObjectId("62dff9584f5b702d61c81c3c"),
"state": [
{
"id": ObjectId("62dffc49cb17483cf0153220"),
"notes": "these are my custom notes!",
"lvl": 3,
},
{
"id": ObjectId("62dffc49cb17483cf0153221"),
"notes": "hello again",
"lvl": 0,
},
]
},
]
My goal is to be able to update and add an element in this array in the following situation:
If the ID of the new element is not in the state array, push the new element in the array
If the ID of the new element is in the state array and its lvl field is 0, update that element with the new information
If the ID of the new element exists in the array, and its lvl field is not 0, then nothing should happen. I will throw an error by seeing that no documents were matched.
Basically, to accomplish this I was thinking about using findOneAndUpdate with upsert, but I am not sure how to tell the query to update the state if lvl is 0 or don't do anything if it is bigger than 0 when the match is found.
For solving (1) this is what I was able to come up with:
db.collection.findOneAndUpdate(
{
"postId": ObjectId("62dffd0acb17483cf015375f"),
"userId": ObjectId("62dff9584f5b702d61c81c3c"),
"state.id": {
"$ne": ObjectId("62dffc49cb17483cf0153222"),
},
},
{
"$push": {"state": {"id": ObjectId("62dffc49cb17483cf0153222"), "lvl": 1}}
},
{
"new": true,
"upsert": true,
}
)
What is the correct way to approach this issue? Should I just split the query into multiple ones?
Edit: as of now I have done this in more than one query (one to fetch the document, then I iterate over its state array to check if the ID exists in it, and then I perform (1), (2) and (3) in a normal if-else clause)
If the ID of the new element exists in the array, and its lvl field is not 0, then nothing should happen. I will throw an error by seeing that no documents where matched.
First thing FYI,
upsert is not possible in the nested array
upsert will not add new elements to the array
upsert can add a new document with the new element
if you want to throw an error if the record does not present then you don't need upsert
Second thing, you can achieve this in one query by using an update with aggregation pipeline in MongoDB 4.2,
Note: Here i must inform you, this query will respond updated document but there will be no flag or any clue if this query fulfilled your first situation or second situation, or the third situation out of 3, you have to check in your client-side code through query response.
check conditions for postId and userId fields only
we are going to update state field under $set stage
check the condition if the provided id is present in state's id?
true, $map to iterate loop of state array
check conditions for id and lvl: 0?
true, $mergeObjects to merge current object with the new information
false, it will not do anything
false, then add that new element in state array, by $concatArrays operator
db.collection.findOneAndUpdate(
{
postId: ObjectId("62dffd0acb17483cf015375f"),
userId: ObjectId("62dff9584f5b702d61c81c3c")
},
[{
$set: {
state: {
$cond: [
{ $in: [ObjectId("62dffc49cb17483cf0153221"), "$state.id"] },
{
$map: {
input: "$state",
in: {
$cond: [
{
$and: [
{ $eq: ["$$this.id", ObjectId("62dffc49cb17483cf0153221")] },
{ $eq: ["$$this.lvl", 0] }
]
},
{
$mergeObjects: [
"$$this",
{
// update your new fields here
"notes": "new note"
}
]
},
"$$this"
]
}
}
},
{
$concatArrays: [
"$state",
[
// add new element
{
"id": ObjectId("62dffc49cb17483cf0153221"),
"lvl": 1
}
]
]
}
]
}
}
}],
{ returnNewDocument: true }
)
Playrgound
Third thing, you can execute 2 update queries,
The first query, for the case: element does not present and it will push a new element in state
let response = db.collection.findOneAndUpdate({
postId: ObjectId("62dffd0acb17483cf015375f"),
userId: ObjectId("62dff9584f5b702d61c81c3c"),
"state.id": { $ne: ObjectId("62dffc49cb17483cf0153221") }
},
{
$push: {
state: {
id: ObjectId("62dffc49cb17483cf0153221"),
lvl: 1
}
}
},
{
returnNewDocument: true
})
The second query on the base of if the response of the above query is null then this query will execute,
This will check state id and lvl: 0 conditions if conditions are fulfilled then execute the update fields operation, it will return null if the document is not found
You can throw if this will return null otherwise do stuff with response data and response success
if (response == null) {
response = db.collection.findOneAndUpdate({
postId: ObjectId("62dffd0acb17483cf015375f"),
userId: ObjectId("62dff9584f5b702d61c81c3c"),
state: {
$elemMatch: {
id: ObjectId("62dffc49cb17483cf0153221"),
lvl: 0
}
}
},
{
$set: {
// add your update fields
"state.$.notes": "new note"
}
},
{
returnNewDocument: true
});
// not found and throw an error
if (response == null) {
return {
// throw error;
};
}
}
// do stuff with "response" data and return result
return {
// success;
};
Note: As per the above options, I would recommend you that I explained in the Third thing that you can execute 2 update queries.
What you're trying became possible with the introduction pipelined updates, here is how I would do it by using $concatArrays to concat the exists state array with the new input and $ifNull in case of an upsert to init the empty value, like so:
const inputObj = {
"id": ObjectId("62dffc49cb17483cf0153222"),
"lvl": 1
};
db.collection.findOneAndUpdate({
"postId": ObjectId("62dffd0acb17483cf015375f"),
"userId": ObjectId("62dff9584f5b702d61c81c3c")
},
[
{
$set: {
state: {
$ifNull: [
"$state",
[]
]
},
}
},
{
$set: {
state: {
$concatArrays: [
{
$map: {
input: "$state",
in: {
$mergeObjects: [
{
$cond: [
{
$and: [
{
$in: [
inputObj.id,
"$state.id"
]
},
{
$eq: [
inputObj.lvl,
0
]
}
]
},
inputObj,
{},
]
},
"$$this"
]
}
}
},
{
$cond: [
{
$not: {
$in: [
inputObj.id,
"$state.id"
]
}
},
[
],
[]
]
}
]
}
}
}
],
{
"new": true,
"upsert": true
})
Mongo Playground
Prior to version 4.2 and the introduction of this feature what you're trying to do was not possible using the naive update syntax, If you are using an older version then you'd have to split this into 2 separate calls, first a findOne to see if the document exists, and only then an update based on that. obviously this can cause stability issue's if you have high update volume.

How to compare fields from different collections in mongodb

Here, I have multiple fields from multiple tables those values needs to compared and need to display desired result.
SQL QUERY:
select pd.service_id,ps.service_id from player pd, service ps where pd.subject_id=ps.subject_id and pd.service_id = ps.service_id
Mongo query:
db.player.aggregate([
{
"$lookup":{
"from":"service",
"localField":"player.subject_id",
"foreignField":"subject_id",
"as":"ps"
}
},
{
"$unwind":"$ps"
},
{
"$match":{
"service_id":{
"$eq": "ps.service_id"
}
}
}
];
sample input records:
player:
[{subject_id:23,service_id:1},{subject_id:76,service_id:9}]
service:
[{subject_id:76,service_id:9},{subject_id:99,service_id:10}]
The match is not working. I have to match service_id's of both collections. Need to get matched records. But not able to see any result. Can anyone please help me to find out the mistake...
In your query, if you want to compare 2 values from the document itself, you need to use $expr operator
{
"$match":{
"$expr":{
"$eq": ["$service_id", "$ps.service_id"]
}
}
}
MongoPlayground
Alternative solution: You need to use Uncorrelated sub-query to "* join" with 2 o more conditions
db.player.aggregate([
{
"$lookup": {
"from": "service",
"let": {
subject_id: "$subject_id",
service_id: "$service_id"
},
"pipeline": [
{
$match: {
$expr: {
$and: [
{
$eq: [
"$$subject_id",
"$subject_id"
]
},
{
$eq: [
"$$service_id",
"$service_id"
]
}
]
}
}
}
],
"as": "ps"
}
},
// Remove non matched results
{
$match: {
"ps.0": {
$exists: true
}
}
},
// Remove temporal "ps" field
{
$addFields: {
"ps": "$$REMOVE"
}
}
])
MongoPlayground

MongoDB4 update nested objects item

i've a problem updating a nested object in MongoDB 4.
I wrote a playground where I'm doing some tests to be able to solve the problem ..
My desiderata is:
I need the "email" item to be overwritten with the lowercase of itself (result obtained).
I need that in all the N objects contained in the "emailsStatus" array the "emailAddress" item is overwritten with itself but in lowercase
I can not carry out the second point, in the playground you will find everything ready and with the test I am carrying out but I am not succeeding .. What am I wrong?
Playground: https://mongoplayground.net/p/tJ25souNlYZ
Try $map to iterate loop of emailsStatus array and convert emailAddress lower case, merge current object with update email field using $mergeObjects,
one more suggestion for query part is you can check single condition for email using $type is string
db.collection.update(
{ email: { $type: "string } },
[{
$set: {
email: { $toLower: "$email" },
emailsStatus: {
$map: {
input: "$emailsStatus",
in: {
$mergeObjects: [
"$$this",
{ emailAddress: { $toLower: "$$this.emailAddress" } }
]
}
}
}
}
}],
{ multi: true }
)
Playground
Check null condition in emailAddress array using $cond and $type,
$map: {
input: "$emailsStatus",
in: {
$mergeObjects: [
"$$this",
{
emailAddress: {
$cond: [
{ $eq: [{ $type: "$$this.emailAddress" }, "string"] },
{ $toLower: "$$this.emailAddress" },
"$$this.emailAddress"
]
}
}
]
}
}
Playground