Automattic / mongoose

MongoDB object modeling designed to work in an asynchronous environment.
https://mongoosejs.com
MIT License
26.88k stars 3.83k forks source link

pre, post middleware are not executed on findByIdAndUpdate #964

Closed skotchio closed 9 years ago

skotchio commented 12 years ago

Because a findAndUpdate method is presented to cut down the following code:

 Model.findById(_id, function (err, doc) {
      if (doc) {
          doc.field = 'value';
          doc.save(function (err) {
                 // do something;
          });
      }
 });

to this:

Model
   .findByIdAndUpdate(_id, {$set: {field: 'value'}}, function (err, doc) {
        // do something 
    });

We need to use pre, post middleware exactly the same. At now pre, post middleware are not executed when I make findByIdAndUpdate.

aheckmann commented 12 years ago

by design. there are docs involved to call hooks on.

aheckmann commented 12 years ago

correction, there are no docs to call hooks on.

skotchio commented 12 years ago

So? If I want to call pre, post middleware I need to use the first approach?

aheckmann commented 12 years ago

yes that is correct. Model.update,findByIdAndUpdate,findOneAndUpdate,findOneAndRemove,findByIdAndRemove are all commands executed directly in the database.

jessefulton commented 11 years ago

This should definitely be clarified in the guide, especially if you're talking about validation in that same page and describing findByIdAndUpdate as "better"

aheckmann commented 11 years ago

if you click on the link "better" it takes you to the full documentation of the method where it explains validation etc.

feel free to send a pull request to add something you feel is better. its pretty easy to do so: https://github.com/LearnBoost/mongoose/blob/master/CONTRIBUTING.md

On Wed, Oct 31, 2012 at 6:03 PM, Jesse Fulton notifications@github.comwrote:

This should definitely be clarified in the guidehttp://mongoosejs.com/docs/documents.html, especially if you're talking about validation in that same page and describing findByIdAndUpdate as "better"

— Reply to this email directly or view it on GitHubhttps://github.com/LearnBoost/mongoose/issues/964#issuecomment-9967865.

Aaron @aaronheckmann https://twitter.com/#!/aaronheckmann

sstadelman commented 10 years ago

Added notes to the middleware doc

Pull request: https://github.com/LearnBoost/mongoose/pull/1750

albanm commented 10 years ago

Hello,

I know the issue is closed, but I am not entirely satisfied with the answer. Shouldn't there be at least a post-save or post-update middleware for findOneAndUpdate and other similar operations ? Is seems ok since the document is returned. Not feasible for pre middlewares or for Model.update, i agree.

It would greatly improve the capabilities of plugins such as mongoosastic that is currently blind to some operations it should be able to support.

If no middleware, does someone have an idea on how to manage some post update operations in a plugin ?

Thanks

jessefulton commented 10 years ago

@albanm certain methods bypass mongoose completely, so you don't get the middleware hooks. AFAIK, the only way to get the hooks to execute is to use separate find() and save() calls as mentioned above.

albanm commented 10 years ago

I get that and it makes sense. Pre middlewares are out of question as there is no fetching prior to certain methods. But still, mongoose should be able to wrap the update operations that also return the updated documents and trigger some post hooks.

beetaa commented 10 years ago

I think @albanm is right, People may want the same functionality when they use the same function. How about wrap those 'directly update' methods with some interception of check that if there's any hooks exist? If hook exist, use it, or call original update methods otherwise.

joafeldmann commented 10 years ago

+1

sunfuze commented 10 years ago

+1

perok commented 10 years ago

+1

ludwiksh commented 10 years ago

+1

cebor commented 10 years ago

:+1:

syzer commented 10 years ago

+1

askhogan commented 10 years ago

:-1:
I get the feature request, but the longer I work with middleware, I find myself needing to bypass middleware often when calling db update scripts and other items that are not routine to my normal operations. By using static methods it becomes very easy to create a custom wrapper that implements the above feature requests already. Since mongoose is now opening to ideas for version 4, it is unlikely that any API change of this magnitude will happen in v3. I request that this be moved to v4 discussion.

I would however, :+1: if I was able to disable middleware on a save call. I often find myself with a mongoose object that was passed from another function and simply want to perform a .save - in this situation, doing a .save is preferable to writing a new query. If this is possible, please point it out

Either way, amazing library. Kudos to awesome maintainers. Not sure how I would operate without it.

roymap commented 10 years ago

+1

sailxjx commented 9 years ago

A simple way to add hooks for these method is overwrite the embed functions:

_update = UserModel.update
UserModel.update = (conditions, update) ->
  update.updatedAt or= new Date
  _update.apply this, arguments

Then every update call of mongoose will fix the updatedAt key of data.

You may give a try of this model limbo. It is a simple wrapper of mongoose model, supporting bind static/method/overwrite to all the schemas, and call rpc methods to query the mongodb.

asinha08 commented 9 years ago

+1, need this feature...

joerx commented 9 years ago

While I sort of understand the reasoning, the lack of hooks on atomic updates IMHO makes Mongoose somewhat pointless in practice. When I use atomic updates any validations, defaults, etc. are not executed, so the entire purpose of using an ODM is defeated. Using find/save will do the job, but is there any guarantee this is always used?

Moreover, usually I would try to avoid find/save since it's not an atomic operation. MongoDB compensates it's lack of transaction support by providing powerful atomic query & update features. So I would use these atomic operations but w/o middleware support Mongoose won't provide much value over the native MongoClient.

Even the examples in http://aaronheckmann.tumblr.com/post/48943525537/mongoose-v3-part-1-versioning would use update, hence bypass middleware. I can use versioning properly or middleware but not combine both? Really, where's the point in having it?

I don't even fully understand the technical reasons: If update & co. wrap around the database operations, why can't we intercept the call and pass the query objects so we can do some validation/customization before we actually do the update?

syzer commented 9 years ago

@joerx +1 would suffice.. :) but your reasoning is flawless.

vkarpov15 commented 9 years ago

The 3.9.x branch has support for pre and post hooks for find and findOne - should be easy to add support for findOneAndUpdate and update.

theGlenn commented 9 years ago

Is this feature merged ?

vkarpov15 commented 9 years ago

So the pre('findOneAndUpdate') and post('findOneAndUpdate') hooks are in master, there isn't an update hook just yet. There's no release that contains either of those yet.

krzysztofantczak commented 9 years ago

So does it triggers pre-save after .update() now?

vkarpov15 commented 9 years ago

No. There is a separate update() hook for Query.update(). Save hooks are distinct from query hooks.

learntoswim commented 9 years ago

@vkarpov15 Can you please link to the supporting documentation for the update hook? Any word on when pre('findOneAndUpdate') and post('findOneAndUpdate') will hit a release?

vkarpov15 commented 9 years ago

@karlstanton use 4.0.0-rc2, npm install mongoose@unstable :)

neverfox commented 9 years ago
'use strict';

var Promise  = require('bluebird');
var mongoose = Promise.promisifyAll(require('mongoose'));

var counterSchema = new mongoose.Schema({
    total: {
        type:    Number,
        default: 0
    }
});

counterSchema.post('findOneAndUpdate', function (doc) {
    console.log(doc.total);
});

var Counter = mongoose.model('Counter', counterSchema);

Promise.coroutine(function *() {
    yield mongoose.connectAsync(process.env.MONGODB_URI);
    console.log('Connected');
    let counter = yield Counter.createAsync({});
    console.log(`${counter.total}`);
    for (let i = 0; i < 10; i++) {
        yield Counter.findOneAndUpdateAsync({ _id: counter.id }, { $inc: { total: 1} });
    }
})();
Connected
0
0
1
2
3
4
5
6
7
8
9

Seems like it's a step behind. What am I missing?

neverfox commented 9 years ago

AH, the new flag default breaking change. While there's something to say about consistency with the underlying driver, I have to say it's really counter-intuitive, especially when considering this new hook.

syzer commented 9 years ago

@neverfox :)

vkarpov15 commented 9 years ago

Yeah @neverfox good catch. This change is documented in the release notes and you can see more discussion why this was changed in #2262.

The reason is that new is false by default in the mongodb node driver, the mongodb shell, the actual mongodb server, etc. and wrapper layers setting non-standard defaults makes life difficult for devs. My canonical example of a module that gets this horribly wrong is gulp-uglify, which overrides a bunch of uglify-js's defaults.

Antignote commented 9 years ago

I see that the issue is closed, but is the functionality in place in version 4.0.2 out is it just still in the unstable version? It doesn't seem to execute on findOneAndUpdate with Scema.pre('update') or Schema.pre('findOneAndUpdate') with the 4.0.2 version. Am I missing something that I have to pass to the function?

vkarpov15 commented 9 years ago

How are you declaring the pre hook @CaptainStaplerz ?

honitus commented 9 years ago

Is findOneAndUpdate middleware hook available in 4.0.2? I upgraded from 3.8 to latest mongoose 4.0.2 to use this and the middlware Document.schema.post('findOneAndUpdate', function (doc) is not getting triggered like save() or remove() does

vkarpov15 commented 9 years ago

@honitus show me your code

honitus commented 9 years ago

@vkarpov15 - Thanks for quick response, here you go

blog.controller,js

// Updates an existing Blog in the DB, adds comment
exports.update = function(req, res) {

Blog.findOneAndUpdate(
     {"_id": req.body._id,},
      {"$push": {"comments": buildComment}},
     {safe: true, upsert: true}, function (err, workspace) {
      if (err) {
         return handleError(res, err);
       }
       return res.send(200);
      }
   );

}

blog.socket.js

/**
 * Broadcast updates to client when the model changes
 */

'use strict';

var Blog= require('./blog.model');

exports.register = function(socket) {
//SAVE WORKS
  Blog.schema.post('save', function (doc) {
    onSave(socket, doc);
  });

// IS NOT TRIGGERED :(
 Blog.schema.post('findOneAndUpdate', function (doc) {
    onComment(socket, doc);
  });

  Blog.schema.post('remove', function (doc) {
    onRemove(socket, doc);
  });
}

//SAVE WORKS when a new blog is created
function onSave(socket, doc, cb) {
  socket.emit('blog:save', doc);
}

// IS NOT TRIGGERED :(
function onComment(socket, doc, cb) {
  socket.emit('blog:findOneAndUpdate', doc);
}

function onRemove(socket, doc, cb) {
  socket.emit('blog:remove', doc);
}
honitus commented 9 years ago

@vkarpov15 Thanks for reopening. Does this mean hook for findOneAndUpdate isn't there in 4.0.2 and you plan to include in 4.0.3.

Antignote commented 9 years ago

@vkarpov15 Here's the code where I declare the hooks:

...
var TodoSchema = new mongoose.Schema({
  name: {type: String, required: true},
  note: String,
  completed: {type: Boolean, default: false},
  updatedAt: {type: Date, default: Date.now},
  user: {
    type: mongoose.Schema.ObjectId,
    ref: 'Users'
  }
});
...
// Not executed
TodoSchema.pre('update', function() {
  console.log('------------->>>>>> update updatedAt')
  this.updatedAt = Date.now();
});
// Not executed
TodoSchema.pre('findOneAndUpdate', function() {
  console.log('------------->>>>>> update updatedAt')
  this.updatedAt = Date.now();
});

And here's where I call the update:

...
router.route('/:id')
.put(function(req, res, next) {
  TodoModel.findOneAndUpdate({_id: req.params.id, user: req.user.id}, req.body, {new: true}, function(err, post) {
    if(err) return next(err);
    if(post) {
      res.status(200).json(post);
    }
    else {
      next(newSystemError(errorCodes.TODO_NOT_FOUND, undefined, req.params.id));
    }
  });
});
vkarpov15 commented 9 years ago

As far as I know the findOneAndUpdate() hook should be in there, if it's not working that's a bug

@CaptainStaplerz try:

TodoSchema.pre('update', function(next) {
  console.log('------------->>>>>> update updatedAt')
  this.updatedAt = Date.now();
  next();
});

Also, is the console.log statement executing, or is it just the Date.now() part that's giving unexpected results?

honitus commented 9 years ago

@vkarpov15 I changed my source code, adding the changes you made in Add implementation #964 https://github.com/Automattic/mongoose/commit/e98ef98e857965c4b2ae3339fdd7eefd2a5a9913

It works like charm now. So I think the fix is not checked into main

vkarpov15 commented 9 years ago

@honitus are you sure you're using mongoose 4.0.2? That change is in fact committed to 4.0.0 and up.

honitus commented 9 years ago

honitus$ npm view mongoose version 4.0.2

vkarpov15 commented 9 years ago

@honitus what you're doing incorrectly is that you're adding hooks to the schema after compiling your model. Model.schema.pre('remove'); is not expected to work, see "Compiling your first model" on the model docs. Attach the hooks to the schema first and then things should work - that's the only difference I see between your code and our tests.

vkarpov15 commented 9 years ago

@CaptainStaplerz the only way I can find to repro your code is with an empty update. The below code works

var mongoose = require('mongoose');
mongoose.set('debug', true);
var util = require('util');

mongoose.connect('mongodb://localhost:27017/gh964');

var TodoSchema = new mongoose.Schema({
  name: {type: String, required: true},
  note: String,
  completed: {type: Boolean, default: false},
  updatedAt: {type: Date, default: Date.now},
  user: {
    type: mongoose.Schema.ObjectId,
    ref: 'Users'
  }
});
TodoSchema.pre('update', function() {
  console.log('------------->>>>>> update updatedAt')
  this.updatedAt = Date.now();
});
TodoSchema.pre('findOneAndUpdate', function() {
  console.log('------------->>>>>> update updatedAt')
  this.updatedAt = Date.now();
});

var Todo = mongoose.model('Todo', TodoSchema);

Todo.update({}, { note: "1" }, function(err) {
  if (err) {
    console.log(err);
  }
  console.log('Done');
  process.exit(0);
});

But once you make the update empty:

var mongoose = require('mongoose');
mongoose.set('debug', true);
var util = require('util');

mongoose.connect('mongodb://localhost:27017/gh964');

var TodoSchema = new mongoose.Schema({
  name: {type: String, required: true},
  note: String,
  completed: {type: Boolean, default: false},
  updatedAt: {type: Date, default: Date.now},
  user: {
    type: mongoose.Schema.ObjectId,
    ref: 'Users'
  }
});
TodoSchema.pre('update', function() {
  console.log('------------->>>>>> update updatedAt')
  this.updatedAt = Date.now();
});
TodoSchema.pre('findOneAndUpdate', function() {
  console.log('------------->>>>>> update updatedAt')
  this.updatedAt = Date.now();
});

var Todo = mongoose.model('Todo', TodoSchema);

Todo.update({}, { }, function(err) {
  if (err) {
    console.log(err);
  }
  console.log('Done');
  process.exit(0);
});

The pre update hook no longer executes. Is this consistent with what you're seeing?

honitus commented 9 years ago

@vkarpov15 Model.schema.pre('remove'); is one the stubs automatically created by angular full stack I added implementation #964 e98ef98 All I did is add one line below this.schema.s.hooks.wrap('findOneAndUpdate', Query.base.findOneAndUpdate, See Line 1526 in 4.0.2 - this.model.hooks.wrap('findOneAndUpdate', Query.base.findOneAndUpdate,

Replace Line 1526 with- this.schema.s.hooks.wrap('findOneAndUpdate', Query.base.findOneAndUpdate, and it works.

vkarpov15 commented 9 years ago

That's by design. Mongoose is not supposed to allow modifying the schema hooks after the model has been compiled (that is, the mongoose.model() call), which is why that no longer works.

Antignote commented 9 years ago

@vkarpov15 I managed to get the hook working now with the following, it must have been a stupid typo or something else:

TodoSchema.pre('findOneAndUpdate', function(next) {
  console.log('------------->>>>>> update updatedAt: ', this.updatedAt);
  this.updatedAt = Date.now();
  next();
});

However 'this' doesn't seem to refer to the updated model (but it does in the 'save' hook?), thereby this.updatedAt refers to undefined.

How do I update the 'updatedAt' -field in the 'findOneAndUpdate' -hook?