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.
Sobre esta tarefa
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
bankenumber.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.
Passos
Criar um índice de múltiplas propriedades
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 campos
bankenumbernã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
Crie um partialFilterExpression
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
nulladiciona 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 }
Teste a implementação do seu banco de dados
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.
Configurar validação de esquema
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 expressão uniqueAccounts compara o tamanho da array accounts original com o tamanho de accountsSet, que é criado pelo $setIntersection de uma versão mapeada de accounts:
A função
$maptransforma cada entrada na arrayaccountspara incluir somente os camposaccounts.bankeaccounts.number.A função
$setIntersectionremove duplicatas tratando a array mapeada como um conjunto.A função
$eqcompara o tamanho da arrayaccountsoriginal e doaccountsSetdeduplicado.
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.