为何Mongoose的Model.save()在then块中无法更新MongoDB文档?
Mongoose更新函数then/catch版本失效问题排查
我编写了两个逻辑相近的Mongoose文档更新函数,使用async/await的版本能够正常更新MongoDB中的Blog文档,但采用then/catch链式调用的版本无法完成更新操作。请帮忙排查then/catch版本的错误原因,并解释为何两者表现不同。
可正常运行的代码(async/await版本)
exports.update = async(req, res) => { const slug = req.params.slug.toLowerCase(); try { let oldBlog=await Blog.findOne({ slug }).exec(); let form = new formidable.IncomingForm(); form.keepExtensions = true; form.parse(req, async(err, fields, files) => { if (err) { return res.status(400).json({ error: 'Image could not upload' }); } let { title, body, categories, tags } = fields; let { photo } = files; title = title && title[0]; body = body && body[0]; categories = categories && categories[0]; tags = tags && tags[0]; photo = photo && photo[0]; let slugBeforeMerge = oldBlog.slug; oldBlog = _.merge(oldBlog, {title,body, categories, tags, photo}); oldBlog.slug = slugBeforeMerge; if (body) { oldBlog.excerpt = smartTrim(body, 320, ' ', ' ...'); oldBlog.desc = stripHtml(body.substring(0, 160)); } if (categories) { oldBlog.categories = categories.split(','); } if (tags) { oldBlog.tags = tags.split(','); } if (photo) { if (files.photo.size > 10000000) { return res.status(400).json({ error: 'Image should be less then 1mb in size' }); } oldBlog.photo.data = fs.readFileSync(photo.filepath); oldBlog.photo.contentType = photo.mimetype; } const result=await oldBlog.save(); res.json(result); }); } catch (err) { return res.status(400).json({ error: errorHandler(err) }); } };
无法运行的代码(then/catch版本)
exports.update = (req, res) => { const slug = req.params.slug.toLowerCase(); Blog.findOne({ slug }) .exec() .then((oldBlog) => { let form = new formidable.IncomingForm(); form.keepExtensions = true; form.parse(req, (err, fields, files) => { if (err) { return res.status(400).json({ error: 'Image could not upload', }); } let { body, title, categories, tags } = fields; let { photo } = files; title = title && title[0]; body = body && body[0]; categories = categories && categories[0]; tags = tags && tags[0]; photo = photo && photo[0]; let slugBeforeMerge = oldBlog.slug; oldBlog=_.merge(oldBlog, {title,body,categories, tags, photo}); oldBlog.slug = slugBeforeMerge; if (title) { oldBlog.title = title; } if (body) { oldBlog.excerpt = smartTrim(body, 320, ' ', ' ...'); oldBlog.desc = stripHtml(body.substring(0, 160)); } if (categories) { oldBlog.categories = categories.split(','); } if (tags) { oldBlog.tags = tags.split(','); } if (photo) { if (photo.size > 10000000) { return res.status(400).json({ error: 'Image should be less then 1mb in size', }); } oldBlog.photo.data = fs.readFileSync(photo.filepath); oldBlog.photo.contentType = photo.mimetype; } }); return oldBlog.save(); }) .then((result) => res.json(result)) .catch((err) => res.status(400).json({ error: errorHandler(err), }) ); };
错误原因分析
then/catch版本失效的核心问题是异步操作时序完全混乱:
form.parse()是异步方法,它的回调函数要等表单解析完成后才会执行,但你在调用form.parse()之后立刻就返回了oldBlog.save()——这时候回调里对oldBlog的所有修改都还没执行,保存的是从数据库查出来的原始对象,自然看不到任何更新。- 另外,回调里的逻辑完全脱离了then链的控制:就算你在回调里调用
save(),它的错误也没法被外层的catch捕获,还可能因为外层已经返回了响应,导致重复响应报错。
两者表现差异的根源
1. async/await版本的时序逻辑
虽然async/await版本也有小缺陷(外层try/catch捕获不到回调内部的错误),但它的await oldBlog.save()是放在form.parse()的回调里的——这确保了只有在完成所有表单解析、对象修改后,才会执行保存操作,所以数据库能拿到最新的修改。
2. then/catch版本的时序失控
then链的执行是同步的:调用form.parse()后,代码会立刻往下走执行return oldBlog.save(),此时回调函数还没触发,oldBlog还是初始状态。等回调里修改oldBlog的时候,保存操作早就完成了,修改根本不会同步到数据库。
then/catch版本的修复方案
要让then/catch版本正常工作,必须把form.parse()包装成Promise,让它融入then链的异步流程:
exports.update = (req, res) => { const slug = req.params.slug.toLowerCase(); Blog.findOne({ slug }) .exec() .then((oldBlog) => { // 将form.parse包装为Promise,让它进入then链的异步流程 return new Promise((resolve, reject) => { let form = new formidable.IncomingForm(); form.keepExtensions = true; form.parse(req, (err, fields, files) => { if (err) { return reject('Image could not upload'); } let { body, title, categories, tags } = fields; let { photo } = files; title = title && title[0]; body = body && body[0]; categories = categories && categories[0]; tags = tags && tags[0]; photo = photo && photo[0]; let slugBeforeMerge = oldBlog.slug; oldBlog = _.merge(oldBlog, {title,body,categories, tags, photo}); oldBlog.slug = slugBeforeMerge; if (title) { oldBlog.title = title; } if (body) { oldBlog.excerpt = smartTrim(body, 320, ' ', ' ...'); oldBlog.desc = stripHtml(body.substring(0, 160)); } if (categories) { oldBlog.categories = categories.split(','); } if (tags) { oldBlog.tags = tags.split(','); } if (photo) { if (photo.size > 10000000) { return reject('Image should be less then 1mb in size'); } oldBlog.photo.data = fs.readFileSync(photo.filepath); oldBlog.photo.contentType = photo.mimetype; } // 所有修改完成后,执行save并resolve结果 resolve(oldBlog.save()); }); }); }) .then((result) => res.json(result)) .catch((err) => res.status(400).json({ error: errorHandler(err), }) ); };
内容的提问来源于stack exchange,提问作者Sanjay Gautam
相关产品推荐
相关产品推荐

