Mongoose: 新功能:虚拟异步

创建于 2017-10-27  ·  42评论  ·  资料来源: Automattic/mongoose

新功能virtual async ,请支持!

const User = new Schema(
  {
    username: {
      type: String,
      index: true,
      unique: true
    },
    encryptedPassword: {
      type: String,
      required: true,
      minlength: 64,
      maxlength: 64
    },
    passwordSalt: {
      type: String,
      required: true,
      minlength: 32,
      maxlength: 32
    }
})

User.virtual('password').set(async function generate(v) {
  this.passwordSalt = await encryptor.salt()
  this.encryptedPassword = await encryptor.hash(v, this.passwordSalt)
})
  const admin = new User({
    username: 'admin',
    password: 'admin'
  })
  admin.save()

最有用的评论

您的代码中有一些非常好的想法,我们已经多次看到此问题,但还没有时间对其进行调查。 不过我喜欢这个想法,会考虑在即将发布的版本中

所有42条评论

目前,我使用了一种肮脏的方式:

User.virtual('password').set(function(v) {
  this.encryptedPassword = v
})

User.pre('validate', function preValidate(next) {
  return this.encryptPassword().then(next)
})

User.method('encryptPassword', async function encryptPassword() {
  this.passwordSalt = await encryptor.salt()
  this.encryptedPassword = await encryptor.hash(
    this.encryptedPassword,
    this.passwordSalt
  )
})

+1

+1

您的代码中有一些非常好的想法,我们已经多次看到此问题,但还没有时间对其进行调查。 不过我喜欢这个想法,会考虑在即将发布的版本中

问题是.. 使用语法是什么样的?

await (user.password = 'some-secure-password');

这不起作用。

根据ECMA262 12.15.4user.password = 'some-secure-password'的返回值应该是 _rval_,在这种情况下是'some-secure-password'

您提议将someVar = object的返回值设为Promise ,根据此线程和上面链接的 ES262 规范,这是“对 ES 语义的严重违反”。

此外,仅仅为了拥有一个方便的功能而尝试实现这样一个违反语义的问题是一个非常糟糕的主意,特别是因为它可能意味着整个猫鼬代码库的各种坏事。

你为什么不这样做:

const hashPassword = require('./lib/hashPassword');

const password = await hashPassword('some-secure-password');
User.password = password; // This is completely normal.

对于这样一个简单的单行,实际上没有必要尝试制作async设置器,这首先不应该完成。

你也可以这样做:

User.methods.setPassword = async function (password) {
  const hashedPassword = await hashPassword(password);
  this.password = hashedPassword;
  await this.save();
  return this;
};
const myUser = new User();
await myUser.setPassword('mypassword...');

我不知道为什么你会遇到做虚拟、预存钩子等的所有麻烦......

我同意@heisian。 恕我直言,这感觉就像功能/api 膨胀。 我看不出这里使用实例方法的替代方法是多么不方便。 但是为此添加一个非常重要的语法支持绝对感觉像膨胀。

我们应该有一个非常简单的功能,如下所示:

User.virtual('password').set((value, done) => {
  encryptValueWithAsyncFunction
    .then(response => done(null, value))
    .catch(reason => done(reason))
  ;
})

@gcanu你完全忽略了我发布的内容,你提议的内容从赋值调用中返回一个 Promise,这完全违反了 Javascript/ECMA262 规范。 为了让您的代码片段正常工作,您的 setter 函数必须是一个 Promise,根据定义,每个规范都不允许这样做,并且无论如何都无法工作。

只是这样做有什么问题:

await User.setPassword('password');

???

如果您以前没有看到,这将不起作用

await (User.password = 'password');

@vkarpov15这不是猫鼬特有的问题,而是质疑当前 ECMAScript 规范的有效性。 这个“功能请求”应该关闭......

下面的代码是非常糟糕的主意! 为什么设置密码包含save操作?

User.methods.setPassword = async function (password) {
  const hashedPassword = await hashPassword(password);
  this.password = hashedPassword;
  await this.save();
  return this;
};

const myUser = new User();
await myUser.setPassword('mypassword...');

猫鼬需要更现代、更优雅。

@heisian好吧,我的错误,我没有注意 setter 的使用...

@heisian请参阅https://github.com/Automattic/mongoose/blob/master/lib/virtualtype.js。

目前,在 Mongoose IMPL 中, gettersetter只是注册一个函数然后调用,它不是https://tc39.github.io/ecma262/#sec -assignment-operators-runtime-semantics - 评估和https://github.com/tc39/ecmascript-asyncawait/issues/82。 那不一样。

所以请打开这个请求。

@fundon ,告诉我这个:你将如何称呼你的虚拟二传手? 请说明用法。 如果您使用async则它必须由承诺处理。 您的原始示例未在 setter/assignment 调用中的任何位置显示await

我的示例代码只是一个例子......你也可以很容易地做到这一点:

User.methods.setPassword = async function (password) {
  const hashedPassword = await hashPassword(password);
  this.password = hashedPassword;
  return this;
};

const myUser = new User();
await myUser.setPassword('mypassword...');
await myUser.save();

明显地..

你的例子对我来说不是一个好方法。

我想要

await new User({ password }).save()

散列密码的方式更简单,更优雅。

为什么? 所以你可以节省几行代码? 原因不足以证明所有额外的工作和可能破坏代码库的更改是合理的。

不管你怎么表达,最终你还必须意识到,Mongoose 内部发生的事情是一个 setter,它不能是 async/await。

我不同意@heisian。 猫鼬的旧东西太多了。 Mongoose 需要重构!
猫鼬需要现代。

如果此问题已关闭。 我会 fork Mongoose,重构它! 再见!

伟大的! 这就是开源的意义所在。 请继续创建带有精简版本的 fork,这对我们所有人都有好处。

真的不用担心需要await (User.password = 'password'); 。 唯一真正的缺点是user.password = 'password';将意味着有一些异步操作正在发生,所以user.passwordSalt不会被设置。 这与钩子的关系也是一个有趣的问题:如果您有pre('validate')pre('save')钩子会发生什么,它们是否应该等到user.password异步操作完成?

我不倾向于不考虑这个问题。 在.save().find()等后面整合异步行为有很多价值。只需确保它与 API 的其余部分完美契合。

今天,异步 getter 和 setter 对我来说非常重要。 我需要从 getter 和 setter 发送 HTTP 请求以使用专有加密方法解密/加密字段,目前,没有办法做到这一点。 您知道如何实现这一目标吗?

@gcanu我只是将这些实现为方法

由于我提到的原因以及有一些方法可以轻松处理您需要的任何异步操作的事实,我没有看到整合async行为背后的任何实用程序......再次, await (User.password = 'password')打破了 ECMAScript 约定,我保证它会很困难并且不值得优雅地实施......

你说得对,这种模式不是我们要实现的。 在保存之前等待 async virtual 解析的想法很有趣。

我会喜欢它的toJSON({virtuals: true})实现。 我通过对数据库运行其他查询获得的一些虚拟字段,我只想在序列化后运行。

@gabzim那会很乱,因为 JSON.stringify 不支持 promise。 所以 res.json() 将永远无法处理异步虚拟,除非你添加额外的 helper 来表达。

啊,是的,有道理,谢谢@vkarpov15

get回调中进行查询是一个好习惯吗?
我认为这在某些情况下会很有用。

假设我想获取网页(或文档)的完整路径,其中可以嵌套文档,例如 Github URL 路径。

const Doc = require('./Doc.js');
//...
subDocSchema.virtual('fullpath').get(async function(){
    const doc = await Doc.findById(this.doc); //doc is a Doc ref of type _id
    return `/${ doc.path }/${ this.path }`
})

这里我们必须使用 async/await,因为查询操作是异步的。

@JulianSoto在这种情况下,我建议您使用方法而不是虚拟virtuals 的主要原因toJSON()toObject()是同步的,因此它们不会为您处理异步虚拟。

我也遇到了一个用例,我正在努力思考一个好的解决方案,对想法非常开放,并同意不破坏 setter 语义。

我编写了一个实用程序来自动将 JSON 补丁集应用于猫鼬模型。 它支持具有深度路径的自动填充: https :

这个想法是,结合一些规则: https :

我的计划是对于简单分配不起作用的情况,使用虚拟。 应用补丁后,虚拟对象将获取您想要的任何内容 - 这将允许您的界面对象与实际的猫鼬模型/db 对象不同。

例如,我有一个支持对“payment_methods”进行“添加”操作的用户对象。 添加支付方式不是直接添加到数组中 - 它是使用支付令牌调用处理器,取回支付方法令牌,以不同方式将其存储在模型中,等等......

但我希望接口模型、概念模型能够使用 JSON 补丁“添加”操作进行修补。

如果没有异步设置器,这将不起作用。 我想唯一的选择是让 mongoose-json-patch 接受路径、操作和猫鼬方法之间的某种映射作为选项,除非有更好的想法?

@claytongulick为什么在异步操作中需要异步设置器而不是await然后同步设置?

@vkarpov15简单地使toObject()toJSON()默认异步并引入toObjectSync()toJSONSync()函数怎么样? Sync变体应该简单地跳过async虚拟。 (我记得这个模式在猫鼬的某个地方使用过,所以它不会太奇怪。)

我的用例是这样的:我有一个模式,它有一个虚拟的,它在另一个模型上执行find() (比简单地填充一个 id 稍微复杂一点)。 当然,我可以使用保存/删除钩子将我想要的东西非规范化到我的主模型中,但这会带来很多可维护性成本(在这种特殊情况下我真的不需要性能优势)。 所以有一个虚拟的东西为我做这件事感觉很自然。

JSON.stringify()不支持异步toJSON() ,所以不幸的是toJSONSync()想法不起作用。

我知道你说你的find()非常复杂,但你可能想看看populate virtuals以防万一。 您也可以尝试查询中间件。

另外,您的 async virtual 是否有 setter 或只有 getter @isamert

对于有此问题的人的解决方案:

在只有 setter 是异步的情况下,我找到了解决方案。 它有点脏,但似乎工作正常。
这个想法是将一个包含承诺解析器作为回调道具和要设置的虚拟属性的对象传递给虚拟设置器。 当 setter 完成后,它会调用回调,这意味着可以保存对象。

要使用受第一个问题启发的基本示例:

const User = new Schema(
  {
    username: {
      type: String,
      index: true,
      unique: true
    },
    encryptedPassword: {
      type: String,
      required: true,
      minlength: 64,
      maxlength: 64
    }
})

User.virtual('password').set(function generate(inputWithCb, virtual, doc) {
  let cb = inputWithCb.cb;
  let password = inputWithCb.password;
  encryptor.hash(password)
  .then((hash) => {
    doc.set("encryptedPassword", hash);
    cb && cb();
  });
})
// create the document
const admin = new User({
  username: 'admin'
});
// setup the promise for setting the async virtuals
const pwdProm = new Promise((resolve) => {
  admin.set("password", {cb: resolve, password: "admin"});
})

//run the promise and save only when the virtual setters have finished executing
pwdProm
.then(() => {
  admin.save();
});

这可能会产生不必要的后果,因此使用风险自负。

@silto为什么不使用返回承诺的模式方法?

@vkarpov15我通常会这样做,但在我这样做的项目中,我有从 json“计划”自动生成的模式、虚拟和 graphQL 端点,所以我更喜欢使用统一的虚拟接口而不是特定情况的方法。

@silto你能提供任何代码示例吗? 我很想看看这是什么样子

在 setter 的情况下,您可能想要保存它或只是查找文档数据,如果保存,那就是承诺,因此您可以检查承诺的字段,解析它们然后保存。

如果要查找数据,可以通过模式选项定义此模型将是 Promise 类型,或者在创建模型时检查模式并检查是否存在作为承诺的 setter、getter 或 virtual,然后转它变成了一个承诺。

或者您可以简单地使用您已经拥有的类似 exec 的函数 (execPopulate)。

在简历中,如果您想观察具有 setter、getter 或 virtual 的数据,您可以为此构建一个函数,如果您想保存数据,它已经是一个承诺,因此您可以使用相同的函数来转换数据在保存之前。

我过去常常使用带有承诺的虚拟,但是当我使用 express-promise 时,几乎所有时间我都不关心承诺,但在某些情况下我使用 Doc。.then(),因为我从来没有使用过带有承诺的 setter 我没有那个问题......

无论哪种方式,最好有某种包装器来获取已经解析的所有数据,并且不必在每个承诺的虚拟和 getter 上使用“then”,在定义了承诺的 setter 之后。

我想我可以用这种方法帮助你。

此致。

PS:使用promisifed virtuals的一个典型例子,在我的例子中我使用2条路径来知道我的Documents是否可以根据外部数据删除或更新,所以我通常需要查询其他模型才能知道这个是否可以删除或修改. 正如我之前所说,express-promise 为我解决了这个问题,但是如果我想在内部检查这些任务是否可以完成,我必须之前解决这个承诺。

@chumager你能提供一些代码示例吗?

嗨,例如,根据我下面的评论,我使用了 2 个虚拟 _update 和 _delete,以及一个定义这些虚拟的插件,以防它在架构中未定义,返回 true。

我有一个模拟模型来定义信用,还有一个项目模式来发布带有 mkt 数据的模拟。
如果存在与模拟关联的项目,则无法删除模拟,并且如果项目已发布用于投资,则无法更新。

模拟中虚拟 _update 的解决方法是通过查找引用了模拟且状态为“En Financiamiento”的项目,如果此查询为真,则模拟无法更新……显然“找到”是一个承诺,所以虚拟它也是......

通常我在前端使用这个虚拟,数据由解析对象的模块解析(co 或 express-promise 取决于一个或结果数组)。

如果我想看文档,我会发现我的virtuals是promises,所以我必须使用co模块来解决,但我已经不得不使用result作为promise...也许只是在结果中添加co会变魔术,或者使用在 find 之后使用 co 的插件......但结果集似乎更自然地完成了这项工作。

我使用很多端点从猫鼬那里获取数据,我必须在任何地方使用该函数或使用 post 钩子来查找。

与 getter 相同,对于 setter,钩子应该是预先验证的,但重要的是不要接触文档中的其他道具,因为它具有循环引用和其他道具,如构造函数。

问候...

PS:如果您真的需要示例代码,请告诉我。

@chumager散文大墙!== 代码示例。 我真的更喜欢代码示例。

此页面是否有帮助?
0 / 5 - 0 等级