Mongoose encontrar / atualizar o subdocumento

Eu tenho os seguintes esquemas para o documento Pasta :

var permissionSchema = new Schema({ role: { type: String }, create_folders: { type: Boolean }, create_contents: { type: Boolean } }); var folderSchema = new Schema({ name: { type: string }, permissions: [ permissionSchema ] }); 

Então, para cada página eu posso ter muitas permissions. No meu CMS, há um painel onde listo todas as pastas e suas permissions. O administrador pode editar uma única permissão e salvá-la.

Eu poderia facilmente salvar todo o documento da pasta com sua matriz de permissions, onde apenas uma permissão foi modificada. Mas eu não quero salvar todo o documento (o esquema real tem muito mais campos) então eu fiz isso:

 savePermission: function (folderId, permission, callback) { Folder.findOne({ _id: folderId }, function (err, data) { var perm = _.findWhere(data.permissions, { _id: permission._id }); _.extend(perm, permission); data.markModified("permissions"); data.save(callback); }); } 

mas o problema é que perm está sempre indefinido ! Eu tentei “estaticamente” buscar a permissão desta maneira:

 var perm = data.permissions[0]; 

e funciona muito bem, então o problema é que a biblioteca Underscore não consegue consultar a matriz de permissions. Então, eu acho que há uma maneira melhor (e workgin) para obter o subdocumento de um documento buscado.

Qualquer ideia?

PS: Eu resolvi checar cada item no array data.permission usando um loop “for” e checando data.permissions [i] ._ id == permission._id mas gostaria de uma solução mais inteligente, sei que tem um!

Então, como você observou, o padrão em mangusto é que quando você “incorpora” dados em uma matriz como essa, obtém um valor _id para cada input da matriz como parte de suas propriedades de sub-documento. Você pode realmente usar esse valor para determinar o índice do item que você pretende atualizar. A maneira do MongoDB de fazer isso é a variável posicional $ operator, que mantém a posição “combinada” na matriz:

 Folder.findOneAndUpdate( { "_id": folderId, "permissions._id": permission._id }, { "$set": { "permissions.$": permission } }, function(err,doc) { } ); 

Esse método .findOneAndUpdate() retornará o documento modificado ou, caso contrário, você poderá usar .update() como método se não precisar do documento retornado. As partes principais são “combinar” o elemento da matriz para atualizar e “identificar” que combina com o $ posicional, como mencionado anteriormente.

Então, é claro que você está usando o operador $set para que apenas os elementos que você especificar sejam realmente enviados “pelo fio” para o servidor. Você pode levar isso adiante com “notação de ponto” e apenas especificar os elementos que você realmente deseja atualizar. Como em:

 Folder.findOneAndUpdate( { "_id": folderId, "permissions._id": permission._id }, { "$set": { "permissions.$.role": permission.role } }, function(err,doc) { } ); 

Portanto, esta é a flexibilidade que o MongoDB oferece, onde você pode ser muito “direcionado” em como você realmente atualiza um documento.

O que isso faz, no entanto, é “ignorar” qualquer lógica que você tenha embutido em seu esquema “mongoose”, como “validação” ou outros “ganchos de pré-salvamento”. Isso ocorre porque a maneira “ideal” é um “recurso” do MongoDB e como ele é projetado. O próprio mangusto tenta ser um invólucro de “conveniência” sobre essa lógica. Mas se você está preparado para assumir algum controle, então as atualizações podem ser feitas da maneira mais ideal.

Então, sempre que possível, mantenha seus dados “incorporados” e não use modelos referenciados. Ele permite a atualização atômica dos itens “pai” e “filho” em atualizações simples, nas quais você não precisa se preocupar com a simultaneidade. Provavelmente é uma das razões pelas quais você deveria ter escolhido o MongoDB em primeiro lugar.

Para validar os subdocumentos ao atualizar no Mongoose, você tem que ‘carregá-lo’ como um object Schema, e então o Mongoose irá automaticamente triggersr a validação e os ganchos.

 const userSchema = new mongoose.Schema({ // ... addresses: [addressSchema], }); 

Se você tiver uma matriz de subdocumentos, você pode buscar o desejado com o método id() fornecido pelo Mongoose. Em seguida, você pode atualizar seus campos individualmente ou, se quiser atualizar vários campos de uma só vez, use o método set() .

 User.findById(userId) .then((user) => { const address = user.addresses.id(addressId); // returns a matching subdocument address.set(req.body); // updates the address while keeping its schema // address.zipCode = req.body.zipCode; // individual fields can be set directly return user.save(); // saves document with subdocuments and triggers validation }) .then((user) => { res.send({ user }); }) .catch(e => res.status(400).send(e)); 

Note que você realmente não precisa do userId para encontrar o documento do usuário, você pode obtê-lo procurando por aquele que tem um subdocumento de endereço que corresponde ao addressId seguinte maneira:

 User.findOne({ 'addresses._id': addressId, }) // .then() ... the same as the example above 

Lembre-se que no MongoDB o subdocumento é salvo somente quando o documento pai é salvo.

Leia mais sobre o tópico na documentação oficial .

Se você não quiser uma coleção separada, apenas incorpore o permissionSchema ao folderSchema.

 var folderSchema = new Schema({ name: { type: string }, permissions: [ { role: { type: String }, create_folders: { type: Boolean }, create_contents: { type: Boolean } } ] }); 

Se você precisar de collections separadas, esta é a melhor abordagem:

Você poderia ter um modelo de permissão:

 var mongoose = require('mongoose'); var PermissionSchema = new Schema({ role: { type: String }, create_folders: { type: Boolean }, create_contents: { type: Boolean } }); module.exports = mongoose.model('Permission', PermissionSchema); 

E um modelo de pasta com uma referência ao documento de permissão. Você pode fazer referência a outro esquema como este:

 var mongoose = require('mongoose'); var FolderSchema = new Schema({ name: { type: string }, permissions: [ { type: mongoose.Schema.Types.ObjectId, ref: 'Permission' } ] }); module.exports = mongoose.model('Folder', FolderSchema); 

Em seguida, chame Folder.findOne().populate('permissions') para solicitar que o mangusto preencha as permissions de campo.

Agora, o seguinte:

 savePermission: function (folderId, permission, callback) { Folder.findOne({ _id: folderId }).populate('permissions').exec(function (err, data) { var perm = _.findWhere(data.permissions, { _id: permission._id }); _.extend(perm, permission); data.markModified("permissions"); data.save(callback); }); } 

O campo perm não será indefinido (se o permission._id estiver na matriz de permissions), já que foi preenchido pelo Mongoose.