In Mongoose, I am seeking to perform atomically a way to Model.FindOne-Or-Insert(), similar functionality and signature as currently available Model.FindOneAndUpdate() except if an instance exists (i.e. matches filter) then do not update using provided object but return instance found as is, and if not exists (i.e. no match with filter) then insert object and return new instance.
I could not find a way using Model.FindOneAndUpdate() not to perform an update to an existing instance by trying out variances to its options and not providing fields to object that preferred not to update if instance exists.
So, my current non-atomic workaround is Model.FindOne() and if not found then perform Document.save().
const Foo = DB.model('foo', FooSchema)
async function findOneAndUpdateFoo(jsonFoo, next) {
const filter = {
deletedAt: null
}
if (jsonFoo.dsAccountId) {
filter.dsAccountId = jsonFoo.dsAccountId
}
if (jsonIntegration.dsUserId) {
filter.dsUserId = jsonIntegration.dsUserId
}
if (jsonFoo.providerId) {
filter.providerId = jsonFoo.providerId
}
const fooDoc = {
name: jsonFoo.name,
dsAccountId: jsonFoo.dsAccountId,
dsUserId: jsonFoo.dsUserId,
providerId: jsonFoo.providerId,
providerName: jsonFoo.providerName,
// Most of these fields could be empty
accessToken: jsonFoo.accessToken,
refreshToken: jsonFoo.refreshToken,
scope: jsonFoo.scope,
tokenType: jsonFoo.tokenType,
expiresAt: jsonFoo.expiresAt
}
return await Foo.findOneAndUpdate(
filter, // find a document with that filter
fooDoc, // document to insert when nothing was found
{ upsert: true, new: true, runValidators: true } // options
)
.catch(next)
}
Suggestions? Thank you
You can use $setOnInsert in your update parameter so that it will only apply in the insert case; with the update becoming a no-op in the case where the document already exists:
return await Foo.findOneAndUpdate(
filter, // find a document with that filter
{$setOnInsert: fooDoc}, // document to insert when nothing was found
{ upsert: true, new: true, runValidators: true }
)
Note that you should also create a unique index over the fields included in your filter and then handle the possibility of a duplicate error. See this post for the details why.
Related
Incase I have an object that looks like the one below
const auth = {
geohash: args.input.geohash,
offenses: args.input.offenses,
online: args.input.online,
paid: args.input.paid,
profilePic: args.input.profilePic,
username: args.input.username,
}
and I pass it inorder to update a document
const update = { _id: mongoose.Types.ObjectId(args._id) }
const value = await DiscoverUsers.findOneAndUpdate(update, auth, { useFindAndModify: false, new: true })
so incase I only want to update the username and I don't want to keep creating a mutation for updating each field in the document.
lets say my mutation looks like this
mutation{
updateDiscoverUsers(_id:"5dab7c198a83f235c89a964a",input:{username:"peter"}){
username
}
}
but this only updates the username but it makes the rest of the fields null but I only want to find a way to only update the fields I have passed in the mutation and the rest remain the same. so I can update the username and profilePic only and the rest remain unchanged.
I would be grateful for the help and thanks in advance
You should use the atomic operator $set to update only where you want, and you should pass only the fields you want to update, not all of them otherwise all the fields are going to be updated by the new value.
like:
const value = await DiscoverUsers.findOneAndUpdate(update, {$set:{username:"pedro"}}, { useFindAndModify: false, new: true })
I'm trying to check that an update command accomplished but when I check for the nModified I'm getting 0 although I do see that the field value changes from one value to another (not kept the same value).
static async updateProfile(username, profileData) {
const usersCollection = db.dbConnection.collection(dbConfig.collectionNames.users);
const updateRes = await usersCollection.update({email: username},
{"$set": {
firstName: profileData.firstName,
lastName: profileData.lastName,
payment: profileData.payment,
}
});
return updateRes.result.nModified > 0;
}
Is there another way to verify the update?
One of the way by findAndModify method:
You can easily compare whole new object and verify each key.
db.getCollection('usertests').findAndModify({
query: {"email":"xxx#xxx.com"},
update: {name: "HHH", "email":"xxx#xxx.com"},
new: true
})
new: true is responsible to return whole updated document. If fail to update it will return null.
Take care here to pass the whole document while update.
update() only return a number of the documents that were successfully updated. So, your logic to check if updated successfully or not is also valid.
Using findAndModify:
I need to get the new resulting document
I need to know if an insert or update was done
var newUpdate = {
$set : newData,
$setOnInsert: {created_at: new Date()}
};
var options = {
upsert :true,
new: true,
};
collectionDriver.findAndModify(colName, query, newUpdate, options, function(err,resultDoc)
{
if (err) {
} else {
}
});
I get the new document, how can I know if an insert/update was happened?
One possible, though not the most efficient way, can be to check for presence of record before issuing the modify command because if you need the new record, then you cannot check if it previously existed.
If a user has an array called "tags":
var User = new Schema({
email: {
type: String,
unique: true,
required: true
},
tags: [{
type: mongoose.Schema.Types.ObjectId,
ref:'Tag',
required: true
}],
created: {
type: Date,
default: Date.now
}
});
and I do a populate('tags') on a query:
User.findById(req.params.id)
.populate("tags")
.exec(function(err, user) { ... });
If one of the tags in the list has actually been deleted, is there a way to remove this dead reference in "tags"?
Currently, the returned user object IS returning the desired result -- ie. only tags that actually exist are in the tags array... however, if I look at the underlying document in mongodb, it still contains the dead tag id in the array.
Ideally, I would like to clean these references up lazily. Does anyone know of a good strategy to do this?
I've tried to find some built-in way to do that but seems that mongoose doesn't provide such functionality.
So I did something like this
User.findById(userId)
.populate('tags')
.exec((err, user) => {
user.tags = user.tags.filter(tag => tag != null);
res.send(user); // Return result as soon as you can
user.save(); // Save user without dead refs to database
})
This way every time you fetch user you also delete dead refs from the document. Also, you can create isUpdated boolean variable to not call user.save if there was no deleted refs.
const lengthBeforeFilter = user.tags.length;
let isUpdated = user.tags.length;
user.tags = user.tags.filter(tag => tag != null);
isUpdated = lengthBeforeFilter > user.tags.length;
res.send(user);
if (isUpdated) {
user.save();
}
Assuming you delete these tags via mongoose, you can use the post middleware.
This will be executed after you've deleted a tag.
tagSchema.post('remove', function(doc) {
//find all users with referenced tag
//remove doc._id from array
});
its sample retainNullValues: true
Example:
User.findById(req.params.id)
.populate({
path: "tag",
options: {
retainNullValues: true
}
})
I had a Tag schema (defined with mongoose):
var Tag = new Schema({
_id: String // Not ObjectId but the name of the tag.
});
I want to use the tag name as its _id, but I don't want to operate this field with name _id. For example, I would like to add a new tag with code new Tag({name: 'tagA'}) instead of new Tag({_id: 'tagA'}). Since the code is more expressive in this way.
So I need to change name to _id. One method would be using the pre-save hook.
Tag.pre('save', function(next) {
if (!this._id && this.name) this._id = this.name;
next();
});
Are there ways better than this one?
This seems to be the best option I found with mongoose for implementing custom primary keys.
<schemaToHook>.pre('save', true, function(next, done) {
// trigger next middleware in parallel
next();
if (!this._id && this.name) {
this._id = this.name;
}
done();
});
I am using a parallel middleware and expecting better performance. Also, while using the above implementation you might want to consider using findOneAndUpdate with upsert = true for INSERT or REPLACE equivalent implementation.
MyModel.findOneAndUpdate(
{foo: 'bar'}, // find a document with that filter
modelDoc, // document to insert when nothing was found
{upsert: true, new: true, runValidators: true}, // options
function (err, doc) { // callback
if (err) {
// handle error
} else {
// handle document
}
}
);