如前面评论中所述,您有两种基本方法来确定某物是否“创建”。这些是为了:
作为一个清单来展示:
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())新数据并在其中抛出“重复键”错误实际上遇到了索引的“唯一”属性。这是一种有效的方法,但在“用户验证”中有一个特殊用例,这是一种方便的逻辑处理,即“验证密码”。
因此,通过username 和password 组合检索用户信息是一种非常常见的模式。在“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: false 或null 结果记录,具体取决于方法。
- 您知道文档(通过组合)通过
updatedExisting: true“存在”或文档返回的位置是“不是null”。
- 如果提供的
password 与 username 已经存在的不匹配,那么您将收到“重复密钥错误”,您可以捕获并相应地响应,建议用户响应“密码不正确”。
所有这些都来自 一个 请求。
这是使用“upserts”而不是简单地向集合抛出插入的主要原因,因为您可以获得不同的逻辑分支,而无需向数据库发出额外请求以确定这些条件中的“哪些”应该是实际的回应。