MongoDB
 sql >> Base de données >  >> NoSQL >> MongoDB

Comment créer un élément s'il n'existe pas et renvoyer une erreur s'il existe

Comme indiqué dans le commentaire précédent, vous disposez de deux approches de base pour déterminer si quelque chose a été "créé" ou non. Il s'agit soit de :

  • Renvoie le rawResult dans la réponse et cochez la case updatedExisting propriété qui vous indique s'il s'agit d'un "upsert" ou non

  • Définir new: false de sorte que "aucun document" n'est réellement renvoyé dans le résultat alors qu'il s'agit en fait d'un "upsert"

Comme une liste pour démontrer :

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()
  }


})()

Et le résultat :

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"
}

Ainsi, le premier cas considère en fait ce code :

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

La plupart des options sont standard ici en tant que "toutes" "upsert" les actions entraîneront l'utilisation du contenu du champ pour "correspondre" (c'est-à-dire le username ) est "toujours" créé dans le nouveau document, vous n'avez donc pas besoin de $set ce champ. Afin de ne pas "modifier" d'autres champs lors de requêtes ultérieures, vous pouvez utiliser $setOnInsert , qui n'ajoute ces propriétés que lors d'un "upsert" action où aucune correspondance n'est trouvée.

Ici le standard new: true est utilisé pour renvoyer le document "modifié" de l'action, mais la différence est dans le rawResult comme indiqué dans la réponse renvoyée :

{
  "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
    }
  }
}

Au lieu d'un "document mangouste", vous obtenez la réponse "brute" réelle du pilote. Le contenu réel du document se trouve sous la "value" propriété, mais c'est le "lastErrorObject" nous intéresse.

Ici, nous voyons la propriété updatedExisting: false . Cela indique qu'"aucune correspondance" n'a été réellement trouvée, donc un nouveau document a été "créé". Vous pouvez donc l'utiliser pour déterminer que la création a réellement eu lieu.

Lorsque vous émettez à nouveau les mêmes options de requête, le résultat sera différent :

{
  "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
    }
  }
}

Le updatedExisting la valeur est maintenant true , et c'est parce qu'il y avait déjà un document qui correspondait au username: 'Bill' dans l'instruction de requête. Cela vous indique que le document était déjà là, vous pouvez donc brancher votre logique pour renvoyer une "Erreur" ou la réponse de votre choix.

Dans l'autre cas, il peut être souhaitable de "ne pas" renvoyer la réponse "brute" et d'utiliser à la place un "document mangouste" renvoyé. Dans ce cas, nous modifions la valeur pour qu'elle soit new: false sans le rawResult option.

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

La plupart des mêmes choses s'appliquent, sauf que maintenant l'action est l'original l'état du document est renvoyé par opposition à l'état "modifié" du document "après" l'action. Par conséquent, lorsqu'aucun document ne correspond réellement à l'instruction "query", le résultat renvoyé est null :

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

Cela vous indique que le document a été "créé", et on peut soutenir que vous savez déjà quel devrait être le contenu du document puisque vous avez envoyé ces données avec la déclaration (idéalement dans le $setOnInsert ). Le fait est que vous savez déjà ce qu'il faut retourner « si » vous avez besoin pour réellement retourner le contenu du document.

En revanche, un document "trouvé" renvoie "l'état d'origine" montrant le document "avant" sa modification :

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

Par conséquent, toute réponse qui n'est pas "null " est donc une indication que le document était déjà présent, et encore une fois vous pouvez bifurquer votre logique en fonction de ce qui a été réellement reçu en réponse.

Ce sont donc les deux approches de base de ce que vous demandez, et elles "fonctionnent" très certainement ! Et tout comme il est démontré et reproductible avec les mêmes déclarations ici.

Addendum - Réserver une clé en double pour les mauvais mots de passe

Il existe une autre approche valable qui est également évoquée dans la liste complète, qui consiste essentiellement à simplement .insert() ( ou .create() à partir de modèles de mangouste) de nouvelles données et ont une erreur de "clé en double" où la propriété "unique" par index est réellement rencontrée. C'est une approche valable, mais il existe un cas d'utilisation particulier dans la "validation de l'utilisateur" qui est un élément pratique de gestion logique, et c'est la "validation des mots de passe".

C'est donc un modèle assez courant pour récupérer les informations de l'utilisateur par le username et password combinaison. Dans le cas d'un "upsert", cette combinaison se justifie comme "unique" et donc un "insert" est tenté si aucune correspondance n'est trouvée. C'est exactement ce qui fait de la correspondance du mot de passe une implémentation utile ici.

Considérez ce qui suit :

    // 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 }
    );

Lors de la première tentative, nous n'avons pas réellement de username pour "Fred" , donc le "upsert" se produirait et toutes les autres choses comme déjà décrites ci-dessus se produiraient pour identifier s'il s'agissait d'une création ou d'un document trouvé.

L'instruction qui suit utilise le même username mais fournit un mot de passe différent de celui qui est enregistré. Ici, MongoDB tente de "créer" le nouveau document car il ne correspondait pas à la combinaison, mais parce que le username devrait être "unique" vous recevez une "Erreur de clé en double":

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

Donc, ce que vous devez réaliser, c'est que vous obtenez maintenant trois conditions à évaluer pour "gratuit". Être :

  • Le "upsert" a été enregistré soit par le updatedExisting: false ou null résultat selon la méthode.
  • Vous savez que le document (par combinaison) "existe" via soit le updatedExisting: true ou où le document renvoyé n'était "pas null ".
  • Si le password fourni ne correspondait pas à ce qui existait déjà pour le username , vous obtiendrez alors "l'erreur de clé en double" que vous pouvez intercepter et répondre en conséquence, informant l'utilisateur en réponse que le "mot de passe est incorrect".

Tout cela de un demande.

C'est le principal raisonnement pour utiliser des "upserts" plutôt que de simplement lancer des insertions dans une collection, car vous pouvez obtenir différentes branches de la logique sans faire de requêtes supplémentaires à la base de données pour déterminer "quelle" de ces conditions devrait être la réponse réelle.