MongoDB
 sql >> Datenbank >  >> NoSQL >> MongoDB

So erstellen Sie ein Element, wenn es nicht vorhanden ist, und geben einen Fehler zurück, wenn es vorhanden ist

Wie bereits im Kommentar erwähnt, haben Sie zwei grundlegende Ansätze, um herauszufinden, ob etwas "erstellt" wurde oder nicht. Diese sind entweder:

  • Gibt das rawResult zurück in der Antwort und überprüfen Sie updatedExisting -Eigenschaft, die Ihnen mitteilt, ob es sich um einen "Upsert" handelt oder nicht

  • Setzen Sie new: false sodass tatsächlich "kein Dokument" als Ergebnis zurückgegeben wird, wenn es sich tatsächlich um einen "Upsert" handelt

Als Auflistung zur Veranschaulichung:

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


})()

Und die Ausgabe:

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

Der erste Fall berücksichtigt also tatsächlich diesen Code:

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

Die meisten Optionen sind hier standardmäßig als "alle" "upsert" Aktionen führen dazu, dass der Feldinhalt zum "Abgleichen" verwendet wird (d.h. der username ) ist "immer" im neuen Dokument erstellt, sodass Sie $set dieses Feld. Um andere Felder bei späteren Anfragen nicht wirklich zu „modifizieren“, können Sie verwenden $setOnInsert , die diese Eigenschaften nur während eines "upsert" hinzufügt Aktion, wenn keine Übereinstimmung gefunden wird.

Hier der Standard new: true wird verwendet, um das "geänderte" Dokument aus der Aktion zurückzugeben, aber der Unterschied liegt im rawResult wie in der zurückgegebenen Antwort gezeigt:

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

Anstelle eines "Mungo-Dokuments" erhalten Sie die eigentliche "rohe" Antwort des Fahrers. Unter dem "value" befindet sich der eigentliche Dokumenteninhalt Eigenschaft, aber es ist das "lastErrorObject" wir interessieren uns für.

Hier sehen wir die Eigenschaft updatedExisting: false . Dies zeigt an, dass tatsächlich "keine Übereinstimmung" gefunden wurde, also ein neues Dokument "erstellt" wurde. So können Sie dies verwenden, um festzustellen, ob die Erstellung tatsächlich stattgefunden hat.

Wenn Sie dieselben Abfrageoptionen erneut eingeben, ist das Ergebnis anders:

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

Die updatedExisting value ist jetzt true , und das liegt daran, dass es bereits ein Dokument gab, das mit username: 'Bill' übereinstimmte in der Abfrageanweisung. Dies sagt Ihnen, dass das Dokument bereits vorhanden war, sodass Sie Ihre Logik verzweigen können, um einen "Fehler" oder eine andere gewünschte Antwort zurückzugeben.

Im anderen Fall kann es wünschenswert sein, die "rohe" Antwort "nicht" zurückzugeben und stattdessen ein zurückgegebenes "Mungo-Dokument" zu verwenden. In diesem Fall ändern wir den Wert auf new: false ohne das rawResult Option.

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

Die meisten der gleichen Dinge gelten, außer dass die Aktion jetzt das Original ist Status des Dokuments zurückgegeben wird, im Gegensatz zum "geänderten" Status des Dokuments "nach" der Aktion. Wenn es also kein Dokument gibt, das tatsächlich mit der "query"-Anweisung übereinstimmt, ist das zurückgegebene Ergebnis null :

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

Dies sagt Ihnen, dass das Dokument "erstellt" wurde, und es ist fraglich, ob Sie bereits wissen, was der Inhalt des Dokuments sein sollte, da Sie diese Daten mit der Anweisung gesendet haben (idealerweise in $setOnInsert ). Der Punkt ist, dass Sie bereits wissen, was Sie zurückgeben müssen, "sollten", Sie benötigen, um den Dokumentinhalt tatsächlich zurückzugeben.

Im Gegensatz dazu gibt ein "gefundenes" Dokument den "Originalzustand" zurück und zeigt das Dokument "bevor" es geändert wurde:

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

Daher ist jede Antwort, die "nicht null ist " ist daher ein Hinweis darauf, dass das Dokument bereits vorhanden war, und Sie können Ihre Logik wieder verzweigen, je nachdem, was tatsächlich als Antwort erhalten wurde.

Das sind also die beiden grundlegenden Ansätze für das, was Sie fragen, und sie "funktionieren" mit Sicherheit! Und genauso ist es mit den gleichen Aussagen hier demonstriert und reproduzierbar.

Nachtrag – Reservieren Sie doppelte Schlüssel für schlechte Passwörter

Es gibt noch einen weiteren gültigen Ansatz, auf den auch in der vollständigen Auflistung hingewiesen wird, nämlich einfach .insert() ( oder .create() von Mongoose-Modellen) neue Daten und haben einen "doppelten Schlüssel"-Fehlerwurf, bei dem die "eindeutige" Eigenschaft nach Index tatsächlich angetroffen wird. Es ist ein gültiger Ansatz, aber es gibt einen bestimmten Anwendungsfall in der "Benutzervalidierung", der ein praktisches Stück logischer Handhabung ist, und das ist die "Validierung von Passwörtern".

Es ist also ein ziemlich verbreitetes Muster, Benutzerinformationen anhand des username abzurufen und password Kombination. Im Falle eines "Upsert" rechtfertigt sich diese Kombination als "eindeutig", und daher wird ein "Insert" versucht, wenn keine Übereinstimmung gefunden wird. Genau das macht den Abgleich des Passworts hier zu einer nützlichen Implementierung.

Beachten Sie Folgendes:

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

Beim ersten Versuch haben wir eigentlich keinen username für "Fred" , also würde der "Upsert" auftreten und all die anderen Dinge, die bereits oben beschrieben wurden, um zu identifizieren, ob es sich um eine Erstellung oder ein gefundenes Dokument handelt.

Die folgende Anweisung verwendet denselben username -Wert, stellt aber ein anderes Passwort bereit als das, was aufgezeichnet wird. Hier versucht MongoDB, das neue Dokument zu „erstellen“, da es nicht auf die Kombination passte, sondern weil der username wird als "unique" erwartet Sie erhalten einen "Duplicate Key Error":

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

Sie sollten sich also darüber im Klaren sein, dass Sie jetzt drei erhalten Bedingungen, um "kostenlos" zu evaluieren. Sein:

  • Der "Upsert" wurde entweder von updatedExisting: false aufgezeichnet oder null Ergebnis je nach Methode.
  • Sie wissen, dass das Dokument (durch Kombination) entweder über updatedExisting: true "existiert". oder wo das Dokument zurückgibt war "nicht null ".
  • Wenn das password bereitgestellt wurde, stimmte nicht mit dem überein, was bereits für username existierte , dann würden Sie den „Duplicate Key Error“ erhalten, den Sie abfangen und entsprechend reagieren können, indem Sie dem Benutzer als Antwort mitteilen, dass das „Passwort falsch“ ist.

All das aus einem Anfrage.

Das ist der Hauptgrund für die Verwendung von "Upserts" im Gegensatz zum einfachen Werfen von Einfügungen auf eine Sammlung, da Sie unterschiedliche Verzweigungen der Logik erhalten können, ohne zusätzliche Anfragen an die Datenbank zu stellen, um zu bestimmen, "welche" dieser Bedingungen die tatsächliche Antwort sein soll.