sql >> Database >  >> NoSQL >> MongoDB

Een item maken als het niet bestaat en een fout retourneren als het bestaat

Zoals eerder opgemerkt in het commentaar, heb je twee basisbenaderingen om te bepalen of iets "gemaakt" is of niet. Deze zijn ofwel:

  • Retourneer het rawResult in het antwoord en controleer de updatedExisting eigenschap die u vertelt of het een "upsert" is of niet

  • Stel new: false in zodat "geen document" daadwerkelijk wordt geretourneerd in resultaat terwijl het eigenlijk een "upsert" is

Als een lijst om te demonstreren:

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


})()

En de uitvoer:

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

Dus het eerste geval beschouwt deze code eigenlijk:

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

De meeste opties zijn hier standaard als "alle" "upsert" acties zullen ertoe leiden dat de veldinhoud wordt gebruikt om te "matchen" (d.w.z. de username ) is "altijd" gemaakt in het nieuwe document, dus u hoeft niet $set dat veld. Om andere velden niet daadwerkelijk te "wijzigen" bij volgende verzoeken, kunt u gebruiken $setOnInsert , die deze eigenschappen alleen toevoegt tijdens een "upsert" actie waarbij geen match is gevonden.

Hier de standaard new: true wordt gebruikt om het "aangepaste" document van de actie te retourneren, maar het verschil zit in de rawResult zoals wordt getoond in het geretourneerde antwoord:

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

In plaats van een "mangoesdocument" krijg je het eigenlijke "ruwe" antwoord van de chauffeur. De daadwerkelijke documentinhoud staat onder de "value" eigenschap, maar het is de "lastErrorObject" waarin we geïnteresseerd zijn.

Hier zien we de eigenschap updatedExisting: false . Dit geeft aan dat er daadwerkelijk "geen overeenkomst" is gevonden, dus is er een nieuw document "gemaakt". Je kunt dit dus gebruiken om te bepalen of de creatie daadwerkelijk heeft plaatsgevonden.

Wanneer u dezelfde query-opties opnieuw uitvoert, zal het resultaat anders zijn:

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

De updatedExisting waarde is nu true , en dit komt omdat er al een document was dat overeenkwam met de username: 'Bill' in de query-instructie. Dit vertelt je dat het document er al was, dus je kunt je logica vertakken om een ​​"Fout" of welk antwoord dan ook te retourneren.

In het andere geval kan het wenselijk zijn om het "onbewerkte" antwoord "niet" te retourneren en in plaats daarvan een geretourneerd "mangoestdocument" te gebruiken. In dit geval variëren we de waarde om new: false . te zijn zonder de rawResult optie.

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

De meeste van dezelfde dingen zijn van toepassing, behalve dat de actie nu de originele . is status van het document wordt geretourneerd in tegenstelling tot de "gewijzigde" status van het document "na" de actie. Dus als er geen document is dat daadwerkelijk overeenkomt met de "query"-instructie, is het geretourneerde resultaat null :

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

Dit vertelt u dat het document is "gemaakt", en het is aannemelijk dat u al weet wat de inhoud van het document zou moeten zijn, aangezien u die gegevens met de instructie hebt verzonden (idealiter in de $setOnInsert ). Punt is dat u al weet wat u moet retourneren "zou" u nodig hebben om de inhoud van het document daadwerkelijk te retourneren.

Een "gevonden" document daarentegen retourneert de "oorspronkelijke staat" en toont het document "vóór" het werd gewijzigd:

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

Daarom is elke reactie die "niet null . is " is daarom een ​​indicatie dat het document al aanwezig was, en nogmaals, je kunt je logica vertakken, afhankelijk van wat er daadwerkelijk als antwoord is ontvangen.

Dus dat zijn de twee basisbenaderingen van wat je vraagt, en ze "werken zeker"! En net zoals hier wordt aangetoond en reproduceerbaar met dezelfde uitspraken.

Addendum - Reserveer dubbele sleutel voor slechte wachtwoorden

Er is nog een geldige benadering waarnaar ook wordt gesuggereerd in de volledige lijst, die in wezen is om simpelweg .insert() ( of .create() van mangoestmodellen) nieuwe gegevens en hebben een "duplicaatsleutel"-foutworp waar de "unieke" eigenschap per index daadwerkelijk wordt aangetroffen. Het is een geldige benadering, maar er is één specifiek gebruiksgeval in "gebruikersvalidatie", wat een handig stukje logica is, en dat is "het valideren van wachtwoorden".

Het is dus een vrij algemeen patroon om gebruikersinformatie op te halen met de username en password combinatie. In het geval van een "upsert" rechtvaardigt deze combinatie als "uniek" en daarom wordt een "insert" geprobeerd als er geen match wordt gevonden. Dit is precies wat het matchen van het wachtwoord hier een nuttige implementatie maakt.

Overweeg het volgende:

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

Bij de eerste poging hebben we niet echt een username voor "Fred" , dus de "upsert" zou plaatsvinden en alle andere dingen zoals hierboven beschreven gebeuren om te identificeren of het een creatie of een gevonden document was.

De instructie die volgt gebruikt dezelfde username waarde, maar geeft een ander wachtwoord dan wat is vastgelegd. Hier probeert MongoDB het nieuwe document te "maken", omdat het niet overeenkwam met de combinatie, maar omdat de username is naar verwachting "unique" u ontvangt een "Duplicate key error":

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

Dus je moet je realiseren dat je nu drie . krijgt voorwaarden te evalueren voor "gratis". Zijn:

  • De "upsert" is geregistreerd door de updatedExisting: false of null resultaat afhankelijk van de methode.
  • U weet dat het document ( door combinatie ) "bestaat" via de updatedExisting: true of waar het document retourneert "niet null ".
  • Als het password opgegeven, kwam niet overeen met wat al bestond voor de username , dan krijgt u de "duplicaat-sleutelfout" die u kunt opsluiten en dienovereenkomstig kunt reageren, waarbij u de gebruiker als antwoord geeft dat het "wachtwoord onjuist is".

Dat alles van één verzoek.

Dat is de belangrijkste reden om "upserts" te gebruiken in plaats van simpelweg inserts naar een verzameling te gooien, omdat je verschillende vertakkingen van de logica kunt krijgen zonder extra verzoeken aan de database te doen om te bepalen "welke" van die voorwaarden het daadwerkelijke antwoord zou moeten zijn.



  1. Repositorypatroon met MongoDB - meerdere werkeenheden met één transactie

  2. De eerste 20 documenten vinden uit verzameling in mongodb

  3. Mongoose Pre-Save Hook wordt geactiveerd, maar slaat geen extra veld op (NIET model.update gebruiken)

  4. Trek en voeg tegelijkertijd toe met mongo