MongoDB
 sql >> Base de Dados >  >> NoSQL >> MongoDB

Como criar um item se não existir e retornar um erro se existir


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 o rawResult na resposta e verifique o updatedExisting propriedade que informa se é um "upsert" ou não

  • Defina new: 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 ou null 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ão null ".
  • Se a password fornecido não correspondia ao que já existia para o username , 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.