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
bank
enumber
.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
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
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
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 }
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 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
naaccounts
array para incluir somente osaccounts.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 daaccounts
array original e doaccountsSet
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.