Docs Menu
Docs Home
/ /
Strategies

Índices únicos y validación de esquema

Para garantizar que su base de datos se adhiera al diseño de su aplicación, puede crear índices estratégicamente para combinar las propiedades del índice con la validación del esquema.

Considere una aplicación que resume las finanzas de un usuario. La página principal muestra el ID del usuario y los saldos de todas sus cuentas bancarias sincronizadas con la aplicación.

La aplicación almacena la información de sus usuarios en una colección llamada usersLa colección users contiene documentos con el siguiente esquema:

db.users.insertOne( {
_id: 1,
name: { first: "john", last: "smith" },
accounts: [
{ balance: 500, bank: "abc", number: "123" },
{ balance: 2500, bank: "universal bank", number: "9029481" }
]
} )

La aplicación requiere las siguientes reglas:

  • Un usuario puede registrarse en la aplicación y no sincronizar una cuenta bancaria.

  • Un usuario identifica una cuenta por sus campos bank y number.

  • Un usuario no puede registrar la misma cuenta para dos usuarios diferentes.

  • Un usuario no puede registrar la misma cuenta varias veces para el mismo usuario.

Para diseñar la base de datos de modo que confine sus documentos a las reglas de la aplicación, combina un índice único y la validación de esquemas en tu base de datos utilizando el siguiente procedimiento.

1

Para hacer cumplir las reglas de la aplicación, cree un índice en los campos accounts.bank y accounts.number con las siguientes características:

  • Para garantizar que los campos bank y number no se repitan, haga que el índice único.

  • Para permitir la indexación de múltiples campos, haga que el índice sea compuesto.

  • Para permitir la indexación de documentos dentro de una matriz, haga que el índice sea del tipo multiclave.

Por lo tanto, crea un índice único multiclave compuesto con la siguiente especificación y opciones:

const specification = { "accounts.bank": 1, "accounts.number": 1 };
const options = { name: "Unique Account", unique: true };
db.users.createIndex(specification, options); // Unique Account
2

El índice, en su estado actual, indexa todos los documentos. Sin embargo, esta implementación puede causar errores al insertar documentos que no tengan los campos accounts.bank o accounts.number.

Por ejemplo, intente insertar los siguientes datos en la colección users:

const user1 = { _id: 1, name: { first: "john", last: "smith" } };
const user2 = { _id: 2, name: { first: "john", last: "appleseed" } };
const account1 = { balance: 500, bank: "abc", number: "123" };
db.users.insertOne(user1);
db.users.insertOne(user2);
{ acknowledged: true, insertedId: 1 }
MongoServerError: E11000 duplicate key error collection: test.users index: Unique Account dup key: { accounts.bank: null, accounts.number: null }

Cuando intenta insertar un documento al que le falta uno o más campos especificados en una colección indexada, MongoDB:

  • Rellena los campos faltantes en el documento insertado

  • establece sus valores en null

  • añade una entrada al índice

Al insertar user1 sin los campos accounts.bank y accounts.number, MongoDB los establece como null y agrega una entrada de índice única. Cualquier inserción posterior que también carezca de alguno de los campos, como user2, genera un error de clave duplicada.

Para evitar esto, utilice una expresión de filtro parcial para que el índice solo incluya los documentos que contengan ambos campos. Para más información, consulte Índice parcial con restricción única. Recree el índice con las siguientes opciones:

const specification = { "accounts.bank": 1, "accounts.number": 1 };
const optionsV2 = {
name: "Unique Account V2",
partialFilterExpression: {
"accounts.bank": { $exists: true },
"accounts.number": { $exists: true }
},
unique: true
};
db.users.drop( {} ); // Delete previous documents and indexes definitions
db.users.createIndex(specification, optionsV2); // Unique Account V2

Pon a prueba tu nueva definición de índice insertando dos usuarios que no contengan los campos accounts.bank y accounts.number:

db.users.insertOne(user1);
db.users.insertOne(user2);
{ acknowledged: true, insertedId: 1 }
{ acknowledged: true, insertedId: 2 }
3

Para asegurarse de que no puede registrar la misma cuenta para dos usuarios diferentes, pruebe el siguiente código:

/* Cleaning the collection */
db.users.deleteMany( {} ); // Delete only documents, keep indexes definitions
db.users.insertMany( [user1, user2] );
/* Test */
db.users.updateOne( { _id: user1._id }, { $push: { accounts: account1 } } );
db.users.updateOne( { _id: user2._id }, { $push: { accounts: account1 } } );
{ acknowledged: true, insertedId: null, matchedCount: 1, modifiedCount: 1, upsertedCount: 0 }
MongoServerError: E11000 duplicate key error collection: test.users index: Unique Account V2 dup key: { accounts.bank: "abc", accounts.number: "123" }

El segundo comando updateOne correctamente devuelve un error, ya que no puedes agregar la misma cuenta para dos usuarios distintos.

Pruebe que la base de datos no le permita agregar la misma cuenta varias veces para el mismo usuario:

/* Cleaning the collection */
db.users.deleteMany( {} ); // Delete only documents, keep indexes definitions
db.users.insertMany( [user1, user2] ); // Re-insert test documents
/* Test */
db.users.updateOne( { _id: user1._id }, { $push: { accounts: account1 } } );
db.users.updateOne( { _id: user1._id }, { $push: { accounts: account1 } } );
db.users.findOne( { _id: user1._id } );
{ acknowledged: true, insertedIds: { '0': 1, '1': 2 } }
{ acknowledged: true, insertedId: null, matchedCount: 1, modifiedCount: 1, upsertedCount: 0 }
{ acknowledged: true, insertedId: null, matchedCount: 1, modifiedCount: 1, upsertedCount: 0 }
_id: 1,
name: { first: 'john', last: 'smith' },
accounts: [
{ balance: 500, bank: 'abc', number: '123' },
{ balance: 500, bank: 'abc', number: '123' }
]

El código devuelto muestra que la base de datos agrega incorrectamente la misma cuenta varias veces al mismo usuario. Este error ocurre porque los índices de MongoDB no duplican entradas estrictamente iguales con los mismos valores de clave que apuntan al mismo documento.

Al insertar account1 por segunda vez en el usuario, MongoDB no crea una entrada de índice, por lo que no hay valores duplicados. Para implementar eficazmente el diseño de su aplicación, su base de datos debería devolver un error si intenta agregar la misma cuenta varias veces al mismo usuario.

4

Para que su aplicación rechace agregar la misma cuenta varias veces al mismo usuario, implemente la validación de esquema. El siguiente código usa el operador para escribir una expresión que verifique si los elementos de un array son $expr únicos:

const accountsSet = {
$setIntersection: {
$map: {
input: "$accounts",
in: { bank: "$$this.bank", number: "$$this.number" }
}
}
};
const uniqueAccounts = {
$eq: [ { $size: "$accounts" }, { $size: accountsSet } ]
};
const accountsValidator = {
$expr: {
$cond: {
if: { $isArray: "$accounts" },
then: uniqueAccounts,
else: true
}
}
};

Cuando { $isArray: "$accounts" } es true, la matriz accounts existe en un documento y MongoDB aplica la lógica de validación uniqueAccounts. Si el documento cumple con la lógica, es válido.

La uniqueAccounts expresión compara el tamaño de la accounts matriz original con el tamaño accountsSet de, que se crea mediante el de una versión $setIntersection mapeada accounts de:

  • La función $map transforma cada entrada en el arreglo accounts para incluir únicamente los campos accounts.bank y accounts.number.

  • La función elimina duplicados al tratar la matriz asignada como un $setIntersection conjunto.

  • La $eq función compara el tamaño de la accounts matriz original y la matriz accountsSet deduplicada.

Si ambos tamaños son iguales, todas las entradas son únicas por accounts.bank y accounts.number, la validación devuelve true. De lo contrario, hay duplicados y la validación falla con un error.

Puede probar la validación de su esquema para asegurarse de que su base de datos no permita agregar la misma cuenta varias veces al mismo usuario:

/* Cleaning the collection */
db.users.drop( {} ); // Delete documents and indexes definitions
db.runCommand( {
collMod: "users", // update collection to use schema validation
validator: accountsValidator
} );
db.users.insertMany( [user1, user2] );
/* Test */
db.users.updateOne( { _id: user1._id }, { $push: { accounts: account1 } } );
db.users.updateOne( { _id: user1._id }, { $push: { accounts: account1 } } );
MongoServerError: Document failed validation
Additional information: {
failingDocumentId: 1,
details: {
operatorName: '$expr',
specifiedAs: {
'$expr': {
'$cond': {
if: { '$and': '$accounts' },
then: { '$eq': [ [Object], [Object] ] },
else: true
}
}
},
reason: 'expression did not match',
expressionResult: false
}
}

El segundo comando updateOne() devuelve un error Document failed validation, que indica que la base de datos ahora rechaza cualquier intento de agregar la misma cuenta varias veces al mismo usuario.

Volver

Garantiza la selectividad de los queries

Obtén una insignia de habilidad

¡Domina los "Fundamentos del diseño de indexación" gratis!

Más información

En esta página