Conforme observado no comentário anterior, você tem duas abordagens básicas para descobrir se algo foi "criado" ou não. Estes são para:
-
Retorne orawResult
na resposta e verifique oupdatedExisting
propriedade que informa se é um "upsert" ou não
-
Definanew: false
para que "nenhum documento" seja realmente retornado como resultado quando na verdade for um "upsert"
Como uma lista para demonstrar:
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()
}
})()
E a saída:
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"
}
Então, o primeiro caso realmente considera este código:
User.findOneAndUpdate(
{ username: 'Bill' },
{ $setOnInsert: { password: 'password' } },
{ upsert: true, new: true, rawResult: true }
)
A maioria das opções é padrão aqui como "todos"
"upsert"
ações resultarão no conteúdo do campo sendo usado para "corresponder" (ou seja, o username
) é "sempre" criado no novo documento, então você não precisa $set
aquele campo. Para não "modificar" outros campos em solicitações subsequentes, você pode usar $setOnInsert
, que só adiciona essas propriedades durante um "upsert"
ação onde nenhuma correspondência é encontrada. Aqui o padrão
new: true
é usado para retornar o documento "modificado" da ação, mas a diferença está no rawResult
como é mostrado na resposta retornada:{
"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
}
}
}
Em vez de um "documento mangusto", você obtém a resposta "crua" real do driver. O conteúdo real do documento está sob o
"value"
propriedade, mas é o "lastErrorObject"
Nós estamos interessados em. Aqui vemos a propriedade
updatedExisting: false
. Isso indica que "nenhuma correspondência" foi realmente encontrada, portanto, um novo documento foi "criado". Então você pode usar isso para determinar que a criação realmente aconteceu. Ao emitir as mesmas opções de consulta novamente, o resultado será diferente:
{
"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
}
}
}
O
updatedExisting
o valor agora é true
, e isso ocorre porque já havia um documento que correspondia ao username: 'Bill'
na instrução de consulta. Isso informa que o documento já estava lá, para que você possa ramificar sua lógica para retornar um "Erro" ou qualquer resposta desejada. No outro caso, pode ser desejável "não" retornar a resposta "crua" e usar um "documento mangusto" retornado. Nesse caso, variamos o valor para
new: false
sem o rawResult
opção. User.findOneAndUpdate(
{ username: 'Ted' },
{ $setOnInsert: { password: 'password' } },
{ upsert: true, new: false }
)
A maioria das mesmas coisas se aplicam, exceto que agora a ação é a original o estado do documento é retornado em oposição ao estado "modificado" do documento "após" a ação. Portanto, quando não há nenhum documento que realmente corresponda à instrução "query", o resultado retornado é
null
:Mongoose: users.findAndModify({ username: 'Ted' }, [], { '$setOnInsert': { password: 'password', __v: 0 } }, { upsert: true, new: false, remove: false, fields: {} })
null // <-- Got null in response :(
Isso informa que o documento foi "criado" e é possível que você já saiba qual deve ser o conteúdo do documento, pois enviou esses dados com a instrução (de preferência no
$setOnInsert
). A questão é que você já sabe o que retornar "se" precisar para realmente retornar o conteúdo do documento. Por outro lado, um documento "encontrado" retorna o "estado original" mostrando o documento "antes" de ser modificado:
{
"_id": "5adfc8696878cfc4992e7639",
"username": "Ted",
"__v": 0,
"password": "password"
}
Portanto, qualquer resposta que não seja
null
" é, portanto, uma indicação de que o documento já estava presente e, novamente, você pode ramificar sua lógica dependendo do que realmente foi recebido em resposta. Então, essas são as duas abordagens básicas para o que você está perguntando, e elas certamente "funcionam"! E assim como é demonstrado e reproduzível com as mesmas declarações aqui.
Adendo - Reservar chave duplicada para senhas incorretas
Há mais uma abordagem válida que também é sugerida na listagem completa, que é essencialmente simplesmente
.insert()
(ou .create()
de modelos de mangusto ) novos dados e ter um erro de "chave duplicada" onde a propriedade "única" por índice é realmente encontrada. É uma abordagem válida, mas há um caso de uso específico em "validação de usuário", que é uma peça útil de manipulação lógica, e é "validar senhas". Portanto, é um padrão bastante comum recuperar informações do usuário pelo
username
e password
combinação. No caso de um "upsert" esta combinação justifica como "única" e, portanto, um "insert" é tentado se nenhuma correspondência for encontrada. Isso é exatamente o que torna a correspondência da senha uma implementação útil aqui. Considere o seguinte:
// 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 }
);
Na primeira tentativa, não temos um
username
para "Fred"
, então o "upsert" ocorreria e todas as outras coisas já descritas acima aconteceriam para identificar se era uma criação ou um documento encontrado. A instrução a seguir usa o mesmo
username
mas fornece uma senha diferente da que está registrada. Aqui o MongoDB tenta "criar" o novo documento, pois não correspondeu à combinação, mas porque o username
espera-se que seja "unique"
você recebe um "erro de chave duplicada":{ MongoError: E11000 duplicate key error collection: thereornot.users index: username_1 dup key: { : "Fred" }
Então, o que você deve perceber é que agora você tem três condições para avaliar "gratuitamente". Ser:
- O "upsert" foi registrado pelo
updatedExisting: false
ounull
resultado dependendo do método. - Você sabe que o documento (por combinação) "existe" por meio de
updatedExisting: true
ou onde o documento retorna foi "nãonull
". - Se a
password
fornecido não correspondia ao que já existia para ousername
, você obterá o "erro de chave duplicada" que poderá interceptar e responder adequadamente, informando ao usuário que a "senha está incorreta".
Tudo isso de um solicitar.
Esse é o principal raciocínio para usar "upserts" em vez de simplesmente lançar inserções em uma coleção, pois você pode obter diferentes ramificações da lógica sem fazer solicitações adicionais ao banco de dados para determinar "qual" dessas condições deve ser a resposta real.