sql >> Base de Datos >  >> NoSQL >> MongoDB

Cómo crear un elemento si no existe y devolver un error si existe

Como se señaló en el comentario anterior, tiene dos enfoques básicos para determinar si algo fue "creado" o no. Estos son para:

  • Devuelve el rawResult en la respuesta y verifique el updatedExisting propiedad que te dice si es un "upsert" o no

  • Establecer new: false de modo que "sin documento" se devuelva como resultado cuando en realidad es un "upsert"

Como listado para demostrar:

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


})()

Y la salida:

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

Entonces, el primer caso en realidad considera este código:

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

La mayoría de las opciones son estándar aquí como "todas" "upsert" las acciones darán como resultado que el contenido del campo se utilice para "coincidir" (es decir, el username ) es "siempre" creado en el nuevo documento, por lo que no necesita $set Ese campo. Para no "modificar" otros campos en solicitudes posteriores, puede usar $setOnInsert , que solo agrega estas propiedades durante un "upsert" acción donde no se encuentra ninguna coincidencia.

Aquí el estándar new: true se usa para devolver el documento "modificado" de la acción, pero la diferencia está en el rawResult como se muestra en la respuesta devuelta:

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

En lugar de un "documento de mangosta", obtiene la respuesta "en bruto" real del controlador. El contenido real del documento está debajo del "value" propiedad, pero es el "lastErrorObject" estamos interesados.

Aquí vemos la propiedad updatedExisting: false . Esto indica que realmente no se encontró "ninguna coincidencia", por lo que se "creó" un nuevo documento. Así que puedes usar esto para determinar que la creación realmente ocurrió.

Cuando vuelva a emitir las mismas opciones de consulta, el 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
    }
  }
}

El updatedExisting el valor ahora es true , y esto se debe a que ya había un documento que coincidía con el username: 'Bill' en la instrucción de consulta. Esto le indica que el documento ya estaba allí, por lo que puede bifurcar su lógica para devolver un "Error" o la respuesta que desee.

En el otro caso, puede ser deseable "no" devolver la respuesta "sin procesar" y usar un "documento de mangosta" devuelto en su lugar. En este caso variamos el valor para que sea new: false sin el rawResult opción.

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

Se aplican la mayoría de las mismas cosas excepto que ahora la acción es la original se devuelve el estado del documento en lugar del estado "modificado" del documento "después" de la acción. Por lo tanto, cuando no hay ningún documento que realmente coincida con la declaración de "consulta", el resultado devuelto es null :

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

Esto le dice que el documento fue "creado", y es discutible que ya sabe cuál debería ser el contenido del documento ya que envió esos datos con la declaración (idealmente en el $setOnInsert ). El punto es que ya sabe qué devolver "debería" necesitar para devolver realmente el contenido del documento.

Por el contrario, un documento "encontrado" devuelve el "estado original" que muestra el documento "antes" de que se modificara:

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

Por lo tanto, cualquier respuesta que sea "no null " es, por lo tanto, una indicación de que el documento ya estaba presente y, nuevamente, puede bifurcar su lógica según lo que realmente se recibió como respuesta.

Entonces, esos son los dos enfoques básicos de lo que está preguntando, ¡y ciertamente "funcionan"! Y tal como se demuestra y reproduce con las mismas declaraciones aquí.

Anexo - Reservar clave duplicada para contraseñas incorrectas

Hay un enfoque más válido que también se insinúa en la lista completa, que es esencialmente simplemente .insert() ( o .create() de mongoose models) nuevos datos y tiene un error de "clave duplicada" donde realmente se encuentra la propiedad "única" por índice. Es un enfoque válido, pero hay un caso de uso particular en la "validación de usuarios", que es una parte útil del manejo de la lógica, y es la "validación de contraseñas".

Por lo tanto, es un patrón bastante común recuperar la información del usuario por el username y password combinación. En el caso de una "inserción superior", esta combinación se justifica como "única" y, por lo tanto, se intenta una "inserción" si no se encuentra ninguna coincidencia. Esto es exactamente lo que hace que hacer coincidir la contraseña sea una implementación útil aquí.

Considere lo siguiente:

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

En el primer intento, en realidad no tenemos un username para "Fred" , por lo que se produciría la "inserción superior" y ocurrirían todas las demás cosas, como ya se describió anteriormente, para identificar si se trataba de una creación o de un documento encontrado.

La declaración que sigue usa el mismo username pero proporciona una contraseña diferente a la registrada. Aquí MongoDB intenta "crear" el nuevo documento ya que no coincidía con la combinación, sino porque el username se espera que sea "unique" recibe un "Error de clave duplicada":

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

Entonces, debes darte cuenta de que ahora obtienes tres condiciones para evaluar para "gratis". Siendo:

  • El "upsert" fue registrado por updatedExisting: false o null resultado según el método.
  • Sabe que el documento (por combinación) "existe" a través de updatedExisting: true o donde el documento devuelve "no null ".
  • Si la password proporcionado no coincidía con lo que ya existía para el username , entonces obtendrá el "error de clave duplicada" que puede detectar y responder en consecuencia, informando al usuario en respuesta que la "contraseña es incorrecta".

Todo eso de uno solicitud.

Ese es el motivo principal para usar "upserts" en lugar de simplemente lanzar inserciones en una colección, ya que puede obtener diferentes ramificaciones de la lógica sin realizar solicitudes adicionales a la base de datos para determinar "cuál" de esas condiciones debería ser la respuesta real.