MongoDB
 sql >> Database >  >> NoSQL >> MongoDB

Come creare un articolo se non esiste e restituire un errore se esiste

Come notato nel commento in precedenza, hai due approcci di base per capire se qualcosa è stato "creato" o meno. Questi sono:

  • Restituisci il rawResult nella risposta e controlla updatedExisting proprietà che ti dice se è un "upsert" o meno

  • Imposta new: false in modo che "nessun documento" venga effettivamente restituito nel risultato quando in realtà è un "upsert"

Come un elenco da dimostrare:

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 l'output:

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

Quindi il primo caso considera effettivamente questo codice:

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

La maggior parte delle opzioni sono standard qui come "tutti" "upsert" le azioni risulteranno nell'utilizzo del contenuto del campo per la "corrispondenza" (cioè il username ) è "sempre" creato nel nuovo documento, quindi non è necessario $set quel campo. Per non "modificare" altri campi nelle richieste successive puoi utilizzare $setOnInsert , che aggiunge queste proprietà solo durante un "upsert" azione in cui non viene trovata alcuna corrispondenza.

Qui lo standard new: true viene utilizzato per restituire il documento "modificato" dall'azione, ma la differenza è nel rawResult come mostrato nella risposta restituita:

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

Invece di un "documento mangusta" ottieni la risposta "grezza" effettiva dal driver. Il contenuto effettivo del documento è sotto il "value" proprietà, ma è il "lastErrorObject" ci interessa.

Qui vediamo la proprietà updatedExisting: false . Ciò indica che "nessuna corrispondenza" è stata effettivamente trovata, quindi è stato "creato" un nuovo documento. Quindi puoi usarlo per determinare che la creazione è effettivamente avvenuta.

Quando emetti di nuovo le stesse opzioni di query, il risultato sarà diverso:

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

Il updatedExisting il valore ora è true , e questo perché esisteva già un documento che corrispondeva al username: 'Bill' nella dichiarazione di interrogazione. Questo ti dice che il documento era già lì, quindi puoi quindi ramificare la tua logica per restituire un "Errore" o qualsiasi risposta desideri.

Nell'altro caso, potrebbe essere desiderabile "non" restituire la risposta "grezza" e utilizzare invece un "documento mangusta" restituito. In questo caso modifichiamo il valore in modo che sia new: false senza il rawResult opzione.

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

La maggior parte delle stesse cose si applicano, tranne per il fatto che ora l'azione è l'originale lo stato del documento viene restituito rispetto allo stato "modificato" del documento "dopo" l'azione. Pertanto, quando non esiste alcun documento che corrisponda effettivamente all'istruzione "query", il risultato restituito è null :

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

Questo ti dice che il documento è stato "creato" ed è discutibile che tu sappia già quale dovrebbe essere il contenuto del documento poiché hai inviato quei dati con l'istruzione ( idealmente nel $setOnInsert ). Il punto è che sai già cosa restituire "dovrebbe" essere necessario per restituire effettivamente il contenuto del documento.

Al contrario, un documento "trovato" restituisce lo "stato originale" mostrando il documento "prima" che fosse modificato:

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

Pertanto qualsiasi risposta che sia "non null " è quindi un'indicazione che il documento era già presente, e ancora una volta puoi diramare la tua logica a seconda di ciò che è stato effettivamente ricevuto in risposta.

Quindi questi sono i due approcci di base a ciò che stai chiedendo e sicuramente "funzionano"! E proprio come è dimostrato e riproducibile con le stesse affermazioni qui.

Addendum - Riserva chiave duplicata per password errate

C'è anche un altro approccio valido che è suggerito nell'elenco completo, che è essenzialmente semplicemente .insert() ( o .create() from mongoose models ) nuovi dati e presentano un errore di "chiave duplicata" in cui viene effettivamente rilevata la proprietà "univoca" per indice. È un approccio valido, ma c'è un caso d'uso particolare nella "convalida dell'utente" che è un pratico strumento di gestione della logica, ovvero la "convalida delle password".

Quindi è un modello abbastanza comune recuperare le informazioni sull'utente tramite il username e password combinazione. Nel caso di un "upsert" questa combinazione si giustifica come "unica" e quindi si tenta un "insert" se non viene trovata alcuna corrispondenza. Questo è esattamente ciò che rende la corrispondenza della password un'utile implementazione qui.

Considera quanto segue:

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

Al primo tentativo non abbiamo effettivamente un username per "Fred" , quindi si verificherebbe il "upsert" e tutte le altre cose come già descritte sopra accadono per identificare se si trattava di una creazione o di un documento trovato.

L'istruzione che segue utilizza lo stesso username value ma fornisce una password diversa da quella registrata. Qui MongoDB tenta di "creare" il nuovo documento poiché non corrispondeva alla combinazione, ma perché il username dovrebbe essere "unique" ricevi un "Errore chiave duplicata":

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

Quindi quello che dovresti capire è che ora ne ottieni tre condizioni da valutare “gratuitamente”. Essere:

  • L'"upsert" è stato registrato da updatedExisting: false o null risultato a seconda del metodo.
  • Sai che il documento (per combinazione) "esiste" tramite updatedExisting: true o dove il documento restituito era "non null ".
  • Se la password fornito non corrispondeva a quanto già esisteva per il username , otterresti l'"errore di chiave duplicata" che puoi intercettare e rispondere di conseguenza, avvisando l'utente in risposta che la "password non è corretta".

Tutto questo da uno richiesta.

Questo è il motivo principale per l'utilizzo di "upsert" invece di lanciare semplicemente inserti in una raccolta, poiché è possibile ottenere diversi ramificazioni della logica senza effettuare richieste aggiuntive al database per determinare "quale" di queste condizioni dovrebbe essere la risposta effettiva.