Menu Docs
Página inicial do Docs
/
Manual do banco de dados
/ /

Índices únicos e validação de esquema

Para garantir que seu banco de dados siga o design do aplicação , é possível criar índices estrategicamente para combinar propriedades de índice com validação de esquema.

Considere um aplicação que resume as contas de um usuário. A página principal do aplicação exibe o ID do usuário e os saldos de todas as suas contas bancárias sincronizadas com o aplicação.

O aplicação armazena suas informações de usuário em uma coleção chamada users. A coleção users contém documentos com o seguinte 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" }
]
} )

O aplicação requer as seguintes regras:

  • Um usuário pode se registrar no aplicação e não sincronizar uma conta bancária.

  • Um usuário identifica uma conta por seus campos bank e number.

  • Um usuário não pode registrar a mesma conta para dois usuários diferentes.

  • Um usuário não pode registrar a mesma conta várias vezes para o mesmo usuário.

Para projetar seu banco de dados de forma que ele limite seus documentos às regras do aplicativo, combine um índice único e a validação de esquema no banco de dados usando o procedimento a seguir.

1

Para impor as regras do aplicativo, crie um índice nos campos accounts.bank e accounts.number com as seguintes características:

  • Para garantir que os bank number campos e não se repitam, torne o índice único.

  • Para permitir a indexação de vários campos, faça o índice composto.

  • Para permitir a indexação de documentos dentro de uma array, crie o índice do tipo multikey.

Portanto, você cria um índice único composto de várias chaves com a seguinte especificação e opções:

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

O índice em seu estado atual indexa todos os documentos. No entanto, essa implementação pode causar erros quando você insere documentos sem os campos accounts.bank ou accounts.number.

Por exemplo, tente inserir os seguintes dados na coleção 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 }

Quando você tenta inserir um documento sem um ou mais campos especificados em uma coleção indexada, MongoDB:

  • preenche os campos ausentes no documento inserido

  • define seus valores para null

  • adiciona uma entrada ao índice

Quando você insere user1 sem os campos accounts.bank e accounts.number, o MongoDB os define como null e adiciona uma entrada de índice único . Qualquer inserção posterior que também não tenha nenhum dos campo, como user2, causa um erro de chave duplicada.

Para evitar isso, use uma expressão de filtro parcial para que o índice inclua apenas documentos que contenham ambos os campos. Para obter mais informações, consulte Índice parcial com restrição única. Recrie o índice usando as seguintes opções:

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

Teste sua nova definição de índice inserindo dois usuários que não contenham os campos accounts.bank e accounts.number:

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

Para garantir que você não possa registrar a mesma conta para dois usuários diferentes, teste o seguinte 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" }

O segundo comando updateOne retorna corretamente um erro, pois você não pode adicionar a mesma conta para dois usuários separados.

Teste se o banco de dados não permite adicionar a mesma conta várias vezes para o mesmo usuário:

/* 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' }
]

O código retornado mostra que o banco de dados adiciona incorretamente a mesma conta várias vezes ao mesmo usuário. Esse erro ocorre porque os índices do MongoDB não duplicam entradas estritamente iguais com os mesmos valores de chave apontando para o mesmo documento.

Quando você insere account1 pela segunda vez no usuário, o MongoDB não cria uma entrada de índice, portanto, não há valores duplicados nele. Para implementar efetivamente o design do aplicação , o banco de dados deve retornar um erro se você tentar adicionar a mesma conta várias vezes ao mesmo usuário.

4

Para fazer com que seu aplicação rejeite a adição da mesma conta várias vezes ao mesmo usuário, implemente a Validação de Esquema. O código a seguir usa o operador $expr para escrever uma expressão e verificar se os itens dentro de uma array são exclusivos:

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

Quando { $isArray: "$accounts" } é true, a array accounts existe em um documento e o MongoDB aplica a lógica de validação uniqueAccounts. Se o documento passar pela lógica, ele é válido.

A uniqueAccounts expressão compara o tamanho da accounts array original com o tamanho accountsSet de, que é criado pelo de uma versão $setIntersection mapeada accounts de:

  • A função transforma cada entrada $map na accounts array para incluir somente os accounts.bank accounts.number campos e.

  • A função $setIntersection remove duplicatas tratando a array mapeada como um conjunto.

  • A $eq função compara o tamanho da accounts array original e do accountsSet deduplicado.

Se ambos os tamanhos forem iguais, todas as entradas forem exclusivas por accounts.bank e accounts.number, então a validação retornará true. Caso contrário, os duplicados estarão presentes e a validação falhará com um erro.

Você pode testar a validação de esquema para garantir que o banco de dados não permita adicionar a mesma conta várias vezes ao mesmo usuário:

/* 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
}
}

O segundo comando updateOne() retorna um erro Document failed validation, indicando que o banco de dados agora rejeita qualquer tentativa de adicionar a mesma conta várias vezes ao mesmo usuário.

Voltar

Garantir a seletividade da query

Receber um selo de habilidade

Domine os "Fundamentos do Design de Indexação" gratuitamente!

Saiba mais

Nesta página