mongoDB / mongoose: único se não for nulo

103

Eu queria saber se existe uma maneira de forçar uma entrada de coleção exclusiva, mas apenas se a entrada não for nula . e Esquema de amostra:

var UsersSchema = new Schema({
    name  : {type: String, trim: true, index: true, required: true},
    email : {type: String, trim: true, index: true, unique: true}
});

'e-mail', neste caso, não é necessário, mas se 'e-mail' for salvo, quero ter certeza de que esta entrada é exclusiva (em um nível de banco de dados).

As entradas vazias parecem obter o valor 'null', portanto, todas as entradas sem e-mail travam com a opção 'exclusivo' (se houver um usuário diferente sem e-mail).

No momento, estou resolvendo no nível do aplicativo, mas adoraria salvar a consulta do banco de dados.

THX

Ezmilhouse
fonte

Respostas:

168

A partir do MongoDB v1.8 +, você pode obter o comportamento desejado de garantir valores únicos, mas permitindo vários documentos sem o campo, definindo a sparseopção como true ao definir o índice. Como em:

email : {type: String, trim: true, index: true, unique: true, sparse: true}

Ou na casca:

db.users.ensureIndex({email: 1}, {unique: true, sparse: true});

Observe que um índice único e esparso ainda não permite vários documentos com um emailcampo com um valor de null, apenas vários documentos sem um emailcampo.

Consulte http://docs.mongodb.org/manual/core/index-sparse/

JohnnyHK
fonte
18
Impressionante! Definitivamente, a melhor resposta para novatos como eu depois de 1.8! NOTA: O Mongoose não atualizará seu índice exclusivo para ser esparso se você apenas adicionar um sparse: true ao seu esquema. Você tem que descartar e adicionar novamente o índice. Não sei se isso é esperado ou um bug.
Adam A
8
"Nota: se o índice já existe no banco de dados, ele não será substituído." - mongoosejs.com/docs/2.7.x/docs/schematypes.html
damphat
Não acho que isso responda à pergunta corretamente, pois alguns documentos sem um campo específico não são o mesmo que alguns documentos com um valor nulo nesse campo (que não podem ser indexados exclusivamente).
kako-nawao
1
@ kako-nawao É verdade, ele só funciona para documentos sem o emailcampo, não onde ele realmente tem o valor null. Veja a resposta atualizada.
JohnnyHK
2
Não funciona com campos ausentes. Talvez o comportamento tenha mudado nas versões posteriores do mongodb. A resposta deve ser atualizada.
Joniba
44

tl; dr

Sim, é possível ter vários documentos com um campo definido como nullou não definido, enquanto impõe valores "reais" exclusivos.

requisitos :

  • MongoDB v3.2 +.
  • Conhecer com antecedência o (s) seu (s) tipo (s) de valor concreto (por exemplo, sempre a stringou objectquando não null).

Se você não estiver interessado nos detalhes, sinta-se à vontade para pular para a implementationseção.

versão mais longa

Para complementar a resposta de @Nolan, começando com MongoDB v3.2, você pode usar um índice exclusivo parcial com uma expressão de filtro.

A expressão de filtro parcial tem limitações. Só pode incluir o seguinte:

  • expressões de igualdade (ou seja, campo: valor ou usando o $eqoperador),
  • $exists: true expressão,
  • $gt, $gte, $lt, $lteExpressões,
  • $type expressões,
  • $and operador apenas no nível superior

Isso significa que a expressão trivial {"yourField"{$ne: null}}não pode ser usada.

No entanto, supondo que seu campo sempre use o mesmo tipo , você pode usar uma $typeexpressão .

{ field: { $type: <BSON type number> | <String alias> } }

MongoDB v3.6 adicionou suporte para especificar vários tipos possíveis, que podem ser passados ​​como uma matriz:

{ field: { $type: [ <BSON type1> , <BSON type2>, ... ] } }

o que significa que permite que o valor seja de vários tipos, quando não null.

Portanto, se quisermos permitir que o emailcampo no exemplo abaixo aceite um stringou, digamos, binary datavalores, uma $typeexpressão apropriada seria:

{email: {$type: ["string", "binData"]}}

implementação

mangusto

Você pode especificá-lo em um esquema mongoose:

const UsersSchema = new Schema({
  name: {type: String, trim: true, index: true, required: true},
  email: {
    type: String, trim: true, index: {
      unique: true,
      partialFilterExpression: {email: {$type: "string"}}
    }
  }
});

ou adicione-o diretamente à coleção (que usa o driver node.js nativo):

User.collection.createIndex("email", {
  unique: true,
  partialFilterExpression: {
    "email": {
      $type: "string"
    }
  }
});

driver mongodb nativo

usando collection.createIndex

db.collection('users').createIndex({
    "email": 1
  }, {
    unique: true,
    partialFilterExpression: {
      "email": {
        $type: "string"
      }
    }
  },
  function (err, results) {
    // ...
  }
);

concha mongodb

usando db.collection.createIndex:

db.users.createIndex({
  "email": 1
}, {
  unique: true, 
  partialFilterExpression: {
    "email": {$type: "string"}
  }
})

Isso permitirá inserir vários registros com um nulle - mail ou sem nenhum campo de e-mail, mas não com a mesma sequência de e-mail.

MasterAM
fonte
Resposta incrível. Você é um salvador.
r3wt 01 de
Essa resposta fez isso por mim também.
Emmanuel NK
1
A maioria das respostas aceitas para esta pergunta envolvem certificar-se de que você não está definindo explicitamente valores nulos para suas chaves indexadas. Que em vez disso sejam indefinidos. Eu estava fazendo isso e ainda recebia o erro (ao usar uniquee sparse). Atualizei meu esquema com esta resposta, eliminei meu índice existente e funcionou perfeitamente.
Phil
Votado para este, porque fornece o conhecimento e as respostas possíveis com base nos cenários mais comuns que se acabaria nesta resposta SO em primeiro lugar. Obrigado pela resposta detalhada! : +1:
anothercoder
6

Apenas uma atualização rápida para aqueles que estão pesquisando este tópico.

A resposta selecionada funcionará, mas você pode considerar o uso de índices parciais.

Alterado na versão 3.2: a partir do MongoDB 3.2, o MongoDB oferece a opção de criar índices parciais. Os índices parciais oferecem um superconjunto da funcionalidade de índices esparsos. Se você estiver usando o MongoDB 3.2 ou posterior, os índices parciais devem ser preferidos aos índices esparsos.

Mais documentação sobre índices parciais: https://docs.mongodb.com/manual/core/index-partial/

Nolan Garrido
fonte
2

Na verdade, apenas o primeiro documento onde o campo "email" como não existe será salvo com sucesso. Os salvamentos subsequentes onde "e-mail" não estiver presente falharão ao fornecer o erro (consulte o trecho de código abaixo). Para saber o motivo, consulte a documentação oficial do MongoDB com relação a índices exclusivos e chaves ausentes aqui em http://www.mongodb.org/display/DOCS/Indexes#Indexes-UniqueIndexes .

  // NOTE: Code to executed in mongo console.

  db.things.ensureIndex({firstname: 1}, {unique: true});
  db.things.save({lastname: "Smith"});

  // Next operation will fail because of the unique index on firstname.
  db.things.save({lastname: "Jones"});

Por definição, o índice exclusivo só pode permitir que um valor seja armazenado apenas uma vez. Se você considerar nulo como um desses valores, ele só pode ser inserido uma vez! Você está correto em sua abordagem, garantindo e validando-a no nível do aplicativo. É assim que pode ser feito.

Você também pode gostar de ler este http://www.mongodb.org/display/DOCS/Querying+and+nulls

Samyak Bhuta
fonte