mongoDB / mongoose: unique si non nul

103

Je me demandais s'il y avait moyen de forcer une entrée de collection unique mais seulement si l'entrée n'est pas nulle . e Exemple de schéma:

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

«email» dans ce cas n'est pas obligatoire mais si «email» est enregistré, je veux m'assurer que cette entrée est unique (au niveau de la base de données).

Les entrées vides semblent avoir la valeur «null» donc chaque entrée sans email se bloque avec l'option «unique» (s'il y a un autre utilisateur sans email).

En ce moment, je le résolve au niveau de l'application, mais j'aimerais enregistrer cette requête de base de données.

THX

ezmilhouse
la source

Réponses:

169

À partir de MongoDB v1.8 +, vous pouvez obtenir le comportement souhaité pour garantir des valeurs uniques mais autoriser plusieurs documents sans le champ en définissant l' sparseoption sur true lors de la définition de l'index. Un péché:

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

Ou dans la coquille:

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

Notez qu'un index unique, rare ne permet pas encore de multiples documents avec un emailchamp avec une valeur de null, que plusieurs documents sans un emailchamp.

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

JohnnyHK
la source
18
Impressionnant! Certainement la meilleure réponse pour les newbs comme moi après la 1.8! REMARQUE: Mongoose ne mettra pas à jour votre index unique pour qu'il soit sparse si vous ajoutez simplement un sparse: true à votre schéma. Vous devez supprimer et rajouter l'index. Je ne sais pas si cela est prévu ou s'il s'agit d'un bug.
Adam A
8
"Remarque: si l'index existe déjà sur la base de données, il ne sera pas remplacé." - mongoosejs.com/docs/2.7.x/docs/schematypes.html
damphat
Je ne pense pas que cela réponde correctement à la question, car quelques documents sans champ spécifique ne sont pas les mêmes que quelques documents avec une valeur nulle sur ce champ (qui ne peut pas être indexé de manière unique).
kako-nawao
1
@ kako-nawao C'est vrai, cela ne fonctionne que pour les documents sans le emailchamp, pas là où il y a réellement une valeur de null. Voir la réponse mise à jour.
JohnnyHK
2
Ne fonctionne pas avec les champs manquants. Peut-être que le comportement a été changé dans les versions ultérieures de mongodb. La réponse doit être mise à jour.
joniba
44

tl; dr

Oui, il est possible d'avoir plusieurs documents avec un champ défini sur nullou non défini, tout en appliquant des valeurs «réelles» uniques.

exigences :

  • MongoDB v3.2 +.
  • Connaître vos types de valeurs concrètes à l'avance (par exemple, toujours a stringou objectquand pas null).

Si vous n'êtes pas intéressé par les détails, n'hésitez pas à passer à la implementationsection.

version plus longue

Pour compléter la réponse de @ Nolan, à partir de MongoDB v3.2, vous pouvez utiliser un index unique partiel avec une expression de filtre.

L'expression de filtre partiel a des limites. Il ne peut inclure que les éléments suivants:

  • expressions d'égalité (ie champ: valeur ou en utilisant l' $eqopérateur),
  • $exists: true expression,
  • $gt, $gte, $lt, $lteExpressions,
  • $type expressions,
  • $and opérateur au niveau supérieur uniquement

Cela signifie que l'expression triviale {"yourField"{$ne: null}}ne peut pas être utilisée.

Cependant, en supposant que votre champ utilise toujours le même type , vous pouvez utiliser une $typeexpression .

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

MongoDB v3.6 a ajouté la prise en charge de la spécification de plusieurs types possibles, qui peuvent être passés sous forme de tableau:

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

ce qui signifie qu'il permet à la valeur d'être de l'un de plusieurs types multiples si ce n'est pas le cas null.

Par conséquent, si nous voulons autoriser le emailchamp de l'exemple ci-dessous à accepter l'une stringou, par exemple, des binary datavaleurs, une $typeexpression appropriée serait:

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

la mise en oeuvre

mangouste

Vous pouvez le spécifier dans un schéma mangouste:

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 ajoutez-le directement à la collection (qui utilise le pilote node.js natif):

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

pilote natif mongodb

en utilisant collection.createIndex

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

coque mongodb

en utilisant db.collection.createIndex:

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

Cela permettra d'insérer plusieurs enregistrements avec un nulle - mail, ou sans champ e-mail du tout, mais pas avec la même chaîne d'e-mail.

MasterAM
la source
Réponse géniale. Tu es un sauveur.
r3wt
Cette réponse l'a fait aussi pour moi.
Emmanuel NK
1
La plupart des réponses acceptées pour cette question impliquent de s'assurer que vous ne définissez pas explicitement des valeurs nulles pour vos clés indexées. Qu'ils soient à la place passés indéfinis. Je faisais cela et j'obtenais toujours l'erreur (en utilisant uniqueet sparse). J'ai mis à jour mon schéma avec cette réponse, j'ai supprimé mon index existant et cela a fonctionné comme un charme.
Phil
Voté pour celui-ci, car il fournit les connaissances et les réponses possibles basées sur les scénarios les plus courants, on se retrouverait sur cette réponse SO en premier lieu. Merci pour la réponse détaillée! : +1:
anothercoder
6

Juste une mise à jour rapide pour ceux qui recherchent ce sujet.

La réponse sélectionnée fonctionnera, mais vous voudrez peut-être envisager d'utiliser des index partiels à la place.

Modifié dans la version 3.2: à partir de MongoDB 3.2, MongoDB offre la possibilité de créer des index partiels. Les index partiels offrent un sur-ensemble des fonctionnalités des index épars. Si vous utilisez MongoDB 3.2 ou version ultérieure, les index partiels doivent être préférés aux index épars.

Plus de doco sur les index partiels: https://docs.mongodb.com/manual/core/index-partial/

Nolan Garrido
la source
2

En fait, seul le premier document où le champ "email" n'existe pas sera enregistré avec succès. Les sauvegardes suivantes où «e-mail» n'est pas présent échoueront tout en donnant une erreur (voir l'extrait de code ci-dessous). Pour cette raison, consultez la documentation officielle de MongoDB concernant les index uniques et les clés manquantes ici à 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"});

Par définition, un index unique ne peut autoriser qu'une seule valeur à être stockée une seule fois. Si vous considérez null comme l'une de ces valeurs, elle ne peut être insérée qu'une seule fois! Vous avez raison dans votre démarche en l'assurant et en la validant au niveau applicatif. Voilà comment cela peut être fait.

Vous pouvez également lire ceci http://www.mongodb.org/display/DOCS/Querying+and+nulls

Samyak Bhuta
la source