Join us at MongoDB.local London on 7 May to unlock new possibilities for your data. Use WEB50 to save 50%.
Register now >
Docs Menu
Docs Home
/ /

Índices únicos y validación de esquema

Para garantizar que tu base de datos esté alineada con el diseño de tu aplicación, puedes crear índices estratégicamente para combinar las propiedades de los índices con la validación del esquema.

Considera una aplicación que resuma las finanzas de un usuario. La página principal de la aplicación muestra la 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 users. La 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 mediante 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, crea el índice unique.

  • Para permitir la indexación de varios campos, haz que el índice sea compuesto.

  • Para permitir la indexación de documentos dentro de un arreglo, haga que el índice sea del tipo multikey.

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 cuando insertas documentos que carecen de los campos accounts.bank o accounts.number.

Por ejemplo, se puede intentar 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

  • agrega 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, utiliza una expresión de filtro parcial para que el índice sólo incluya los documentos que contengan ambos campos. Para obtener más información, consulte Índice parcial con restricción única. Recrea el índice usando 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 garantizar que no pueda 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.

Cuando insertas account1 por segunda vez en el usuario, MongoDB no crea una entrada de índice, por lo que no hay valores duplicados en él. Para implementar eficazmente el diseño de tu aplicación, tu base de datos debe devolver un error si intentas agregar la misma cuenta varias veces al mismo usuario.

4

Para que tu aplicación rechace agregar la misma cuenta varias veces al mismo usuario, implementa la Validación de Esquema. El siguiente código utiliza el operador $expr para escribir una expresión que verifique si los elementos dentro de un arreglo son ú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, el arreglo accounts existe en un documento, y MongoDB aplica la lógica de validación uniqueAccounts. Si el documento supera 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 $setIntersection elimina duplicados al tratar el arreglo mapeado como un conjunto.

  • La función $eq compara el tamaño del arreglo original accounts y del accountsSet desduplicado.

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

Puede probar la validación de 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