【问题标题】:How to create item if not exists and return an error if exists如果不存在如何创建项目,如果存在则返回错误
【发布时间】:2018-04-25 09:37:38
【问题描述】:

我正在编写 alexa 技能并想检查用户是否存在于 MongoDB 中。我的代码有效,但如果用户已经在数据库中,我不知道如何定义情况:(

每当我执行代码时,我都会得到: “你好安娜,你是新来的”

我的用户 Anna 保存在 MongoDB 中

但我想区分我的用户何时已经在数据库中并对此做出反应。

有聪明人能解决我的问题吗?

    var myName = "Anan1";
    var userID = this.event.session.user.userId;
    console.log(userID);

    self = this;
    User.findOneAndUpdate(
        {userId:  userID},
        {$set:{name:myName}},
        {upsert: true, new: false, runValidators: true},
        function(err, doc){
            if(err){
                console.log("eeoror");
            }

            console.log(doc);
            if (doc==null){
                self.emit(':ask',
                    "Hello "+ myName +"you are new here")
            }else {
                self.emit(':ask',
                    "Hello "+ myName +"you are not new here")
            }

        });

【问题讨论】:

  • 好吧,您可以使用new: false 并简单地检查返回的文档是否为null。即不是null 表示它以前存在,null 表示它是创建的。在这种情况下,您基本上应该拥有所有文档详细信息,因此您可能不需要匹配的回报。或者,您可以询问rawResponse,它应该表明upsert 是否发生。另外,您可能真的很想在这里$setOnInsert
  • 它不起作用:(我总是你好,你不是新来的。我更新了我的代码,你能看一下吗:)

标签: node.js mongodb mongoose


【解决方案1】:

如前面评论中所述,您有两种基本方法来确定某物是否“创建”。这些是为了:

  • 在响应中返回 rawResult 并检查 updatedExisting 属性,它会告诉您它是否是“upsert”

  • 设置new: false,这样当它实际上是一个“upsert”时,结果中实际上会返回“no document”

作为一个清单来展示:

const { Schema } = mongoose = require('mongoose');

const uri = 'mongodb://localhost/thereornot';

mongoose.set('debug', true);
mongoose.Promise = global.Promise;

const userSchema = new Schema({
  username: { type: String, unique: true },   // Just to prove a point really
  password: String
});

const User = mongoose.model('User', userSchema);

const log = data => console.log(JSON.stringify(data, undefined, 2));

(async function() {

  try {

    const conn = await mongoose.connect(uri);

    await Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()));

    // Shows updatedExisting as false - Therefore "created"

    let bill1 = await User.findOneAndUpdate(
      { username: 'Bill' },
      { $setOnInsert: { password: 'password' } },
      { upsert: true, new: true, rawResult: true }
    );
    log(bill1);

    // Shows updatedExisting as true - Therefore "existing"

    let bill2 = await User.findOneAndUpdate(
      { username: 'Bill' },
      { $setOnInsert: { password: 'password' } },
      { upsert: true, new: true, rawResult: true }
    );
    log(bill2);

    // Test with something like:
    // if ( bill2.lastErrorObject.updatedExisting ) throw new Error("already there");


    // Return will be null on "created"
    let ted1 = await User.findOneAndUpdate(
      { username: 'Ted' },
      { $setOnInsert: { password: 'password' } },
      { upsert: true, new: false }
    );
    log(ted1);

    // Return will be an object where "existing" and found
    let ted2 = await User.findOneAndUpdate(
      { username: 'Ted' },
      { $setOnInsert: { password: 'password' } },
      { upsert: true, new: false }
    );
    log(ted2);

    // Test with something like:
    // if (ted2 !== null) throw new Error("already there");

    // Demonstrating "why" we reserve the "Duplicate" error
    let fred1 = await User.findOneAndUpdate(
      { username: 'Fred', password: 'password' },
      { $setOnInsert: { } },
      { upsert: true, new: false }
    );
    log(fred1);       // null - so okay

    let fred2 = await User.findOneAndUpdate(
      { username: 'Fred', password: 'badpassword' }, // <-- dup key for wrong password
      { $setOnInsert: { } },
      { upsert: true, new: false }
    );

    mongoose.disconnect();

  } catch(e) {
    console.error(e)
  } finally {
    process.exit()
  }


})()

还有输出:

Mongoose: users.remove({}, {})
Mongoose: users.findAndModify({ username: 'Bill' }, [], { '$setOnInsert': { password: 'password', __v: 0 } }, { upsert: true, new: true, rawResult: true, remove: false, fields: {} })
{
  "lastErrorObject": {
    "n": 1,
    "updatedExisting": false,
    "upserted": "5adfc8696878cfc4992e7634"
  },
  "value": {
    "_id": "5adfc8696878cfc4992e7634",
    "username": "Bill",
    "__v": 0,
    "password": "password"
  },
  "ok": 1,
  "operationTime": "6548172736517111811",
  "$clusterTime": {
    "clusterTime": "6548172736517111811",
    "signature": {
      "hash": "AAAAAAAAAAAAAAAAAAAAAAAAAAA=",
      "keyId": 0
    }
  }
}
Mongoose: users.findAndModify({ username: 'Bill' }, [], { '$setOnInsert': { password: 'password', __v: 0 } }, { upsert: true, new: true, rawResult: true, remove: false, fields: {} })
{
  "lastErrorObject": {
    "n": 1,
    "updatedExisting": true
  },
  "value": {
    "_id": "5adfc8696878cfc4992e7634",
    "username": "Bill",
    "__v": 0,
    "password": "password"
  },
  "ok": 1,
  "operationTime": "6548172736517111811",
  "$clusterTime": {
    "clusterTime": "6548172736517111811",
    "signature": {
      "hash": "AAAAAAAAAAAAAAAAAAAAAAAAAAA=",
      "keyId": 0
    }
  }
}
Mongoose: users.findAndModify({ username: 'Ted' }, [], { '$setOnInsert': { password: 'password', __v: 0 } }, { upsert: true, new: false, remove: false, fields: {} })
null
Mongoose: users.findAndModify({ username: 'Ted' }, [], { '$setOnInsert': { password: 'password', __v: 0 } }, { upsert: true, new: false, remove: false, fields: {} })
{
  "_id": "5adfc8696878cfc4992e7639",
  "username": "Ted",
  "__v": 0,
  "password": "password"
}

所以第一种情况实际上考虑了这段代码:

User.findOneAndUpdate(
  { username: 'Bill' },
  { $setOnInsert: { password: 'password' } },
  { upsert: true, new: true, rawResult: true }
)

这里的大多数选项都是标准的,因为 "all" "upsert" 操作将导致用于“匹配”的字段内容(即 username )为 “always” 在新文档中创建,因此您不需要 $set 那个字段。为了不在后续请求中实际“修改”其他字段,您可以使用 $setOnInsert,它只会在找不到匹配项的 "upsert" 操作期间添加这些属性。

这里标准的new: true 用于从操作返回“已修改”文档,但不同之处在于rawResult,如返回的响应所示:

{
  "lastErrorObject": {
    "n": 1,
    "updatedExisting": false,
    "upserted": "5adfc8696878cfc4992e7634"
  },
  "value": {
    "_id": "5adfc8696878cfc4992e7634",
    "username": "Bill",
    "__v": 0,
    "password": "password"
  },
  "ok": 1,
  "operationTime": "6548172736517111811",
  "$clusterTime": {
    "clusterTime": "6548172736517111811",
    "signature": {
      "hash": "AAAAAAAAAAAAAAAAAAAAAAAAAAA=",
      "keyId": 0
    }
  }
}

您从驱动程序获得实际的“原始”响应,而不是“猫鼬文档”。实际文档内容在"value"属性下,但我们感兴趣的是"lastErrorObject"

在这里我们看到属性updatedExisting: false。这表明实际上没有找到“不匹配”,因此“创建”了一个新文档。因此,您可以使用它来确定实际发生的创建。

当你再次发出相同的查询选项时,结果会有所不同:

{
  "lastErrorObject": {
    "n": 1,
    "updatedExisting": true             // <--- Now I'm true
  },
  "value": {
    "_id": "5adfc8696878cfc4992e7634",
    "username": "Bill",
    "__v": 0,
    "password": "password"
  },
  "ok": 1,
  "operationTime": "6548172736517111811",
  "$clusterTime": {
    "clusterTime": "6548172736517111811",
    "signature": {
      "hash": "AAAAAAAAAAAAAAAAAAAAAAAAAAA=",
      "keyId": 0
    }
  }
}

updatedExisting 的值现在是true,这是因为在查询语句中已经有一个与username: 'Bill' 匹配的文档。这告诉您文档已经存在,因此您可以分支您的逻辑以返回“错误”或您想要的任何响应。

在另一种情况下,可能希望“不”返回“原始”响应并使用返回的“猫鼬文档”。在这种情况下,我们将值更改为 new: false 而不使用 rawResult 选项。

User.findOneAndUpdate(
  { username: 'Ted' },
  { $setOnInsert: { password: 'password' } },
  { upsert: true, new: false }
)

大多数相同的事情都适用,除了现在该操作是返回文档的原始状态,而不是“在”操作“之后”文档的“修改”状态。因此当没有文档与“查询”语句实际匹配时,返回结果为null

Mongoose: users.findAndModify({ username: 'Ted' }, [], { '$setOnInsert': { password: 'password', __v: 0 } }, { upsert: true, new: false, remove: false, fields: {} })
null           // <-- Got null in response :(

这告诉您文档是“创建”的,并且可以说您已经知道文档的内容应该是什么,因为您使用语句发送了该数据(最好是在 $setOnInsert 中)。重点是,您已经知道“应该”返回什么才能实际返回文档内容。

相比之下,“找到”文档返回“原始状态”,显示文档“修改之前”:

{
  "_id": "5adfc8696878cfc4992e7639",
  "username": "Ted",
  "__v": 0,
  "password": "password"
}

因此,任何“不是null”的响应都表明该文档已经存在,并且您可以再次根据实际收到的响应来分支您的逻辑。

所以这是您所要求的两种基本方法,它们肯定“有效”!就像这里用相同的语句演示和重现一样。


附录 - 为错误密码保留重复密钥

完整列表中还暗示了另一种有效的方法,它本质上是简单地.insert()(或来自猫鼬模型的.create())新数据并在其中抛出“重复键”错误实际上遇到了索引的“唯一”属性。这是一种有效的方法,但在“用户验证”中有一个特殊用例,这是一种方便的逻辑处理,即“验证密码”。

因此,通过usernamepassword 组合检索用户信息是一种非常常见的模式。在“upsert”的情况下,此组合证明为“唯一”,因此如果未找到匹配项,则尝试“插入”。这正是使匹配密码成为一个有用的实现的原因。

考虑以下几点:

    // Demonstrating "why" we reserve the "Duplicate" error
    let fred1 = await User.findOneAndUpdate(
      { username: 'Fred', password: 'password' },
      { $setOnInsert: { } },
      { upsert: true, new: false }
    );
    log(fred1);       // null - so okay

    let fred2 = await User.findOneAndUpdate(
      { username: 'Fred', password: 'badpassword' }, // <-- dup key for wrong password
      { $setOnInsert: { } },
      { upsert: true, new: false }
    );

在第一次尝试时,我们实际上并没有为"Fred" 提供username,因此会发生“upsert”,并且上面已经描述的所有其他事情都会发生以识别它是创建文件还是找到的文件.

下面的语句使用相同的username 值,但为记录的内容提供不同的密码。此处 MongoDB 尝试“创建”新文档,因为它与组合不匹配,但由于 username 预计为 "unique",您会收到“重复键错误”:

{ MongoError: E11000 duplicate key error collection: thereornot.users index: username_1 dup key: { : "Fred" }

所以你应该意识到你现在有三个条件来评估“免费”。存在:

  • “upsert”由updatedExisting: falsenull 结果记录,具体取决于方法。
  • 您知道文档(通过组合)通过updatedExisting: true“存在”或文档返回的位置是“不是null”。
  • 如果提供的 passwordusername 已经存在的不匹配,那么您将收到“重复密钥错误”,您可以捕获并相应地响应,建议用户响应“密码不正确”。

所有这些都来自 一个 请求。

这是使用“upserts”而不是简单地向集合抛出插入的主要原因,因为您可以获得不同的逻辑分支,而无需向数据库发出额外请求以确定这些条件中的“哪些”应该是实际的回应。

【讨论】:

    【解决方案2】:

    听起来你真正想要的是 unique key constraint 而不是 upsert。

    可以使用架构字段选项在 [mongoose] 中设置唯一键:

    const s = new Schema({ name: { type: String, unique: true }});
    

    或通过index 方法:

    Schema.path('name').index({ unique: true });
    

    如果尝试创建一个已包含该键条目的文档,则会引发错误:

    注意:违反约束在保存时从 MongoDB 返回 E11000 错误,而不是 Mongoose 验证错误。

    【讨论】:

    • 这真的无关紧要,因为该操作已经声明使用upsert 作为选项。您不能在“upsert”上抛出 Duplicate key 错误,其中指定的“唯一”字段用作确定文档是否存在或是否已创建的查询约束。 OP 实际上是在问其他问题,本质上是“我如何判断它是否被创建”。对于此操作,“upserts”通常比捕获错误更“聪明”。查看我回答的列表。
    • @NeilLunn 我礼貌地不同意。我的回答是基于 OP 用来描述他们意图的标题。假设这是正确的,那么他们使用 upsert 的实现将完全不正确,因此我对“不是 upsert”的注释。关于您认为upsert“通常有点'聪明'”的假设,这伴随着一连串的警告。不用说,假设可能很危险。
    • 我真的认为您应该完整阅读我的答案,因为您可能会学到一些东西。这个逻辑背后有充分的理由。
    • @NeilLunn 假设我“可能会学到一些东西”是一个非常有趣的观点。但是可以肯定的是,假设我可以从您的回答中学到一些东西,那么我假设您也可以学到一些东西。例如,在未来某个时间,需要在此实现之外创建用户的另一个过程,而实现者不知道这种特殊处理。这可能是创建重复记录的严重风险。另一方面,鉴于 MongoDB 提供了一种内置方法来防止这种情况发生,无论它是如何实现的,为什么重新发明轮子会“更聪明”呢?
    • IMO 这是正确的答案。它们的键应该是唯一的。
    猜你喜欢
    • 2016-01-18
    • 1970-01-01
    • 2013-05-26
    • 1970-01-01
    • 2020-12-23
    • 2012-02-04
    • 1970-01-01
    • 2016-02-02
    相关资源
    最近更新 更多