Point MongoDB (.) Dans le nom de la clé


94

Il semble que mongo n'autorise pas l'insertion de clés avec un point (.) Ou un signe dollar ($), mais lorsque j'ai importé un fichier JSON contenant un point à l'aide de l'outil mongoimport, cela fonctionnait bien. Le conducteur se plaint d'avoir tenté d'insérer cet élément.

Voici à quoi ressemble le document dans la base de données:

{
    "_id": {
        "$oid": "..."
    },
    "make": "saab",
    "models": {
        "9.7x": [
            2007,
            2008,
            2009,
            2010
        ]
    }
}

Est-ce que je fais tout cela mal et ne devrais-je pas utiliser des cartes de hachage comme celles-ci avec des données externes (c'est-à-dire les modèles) ou puis-je échapper au point d'une manière ou d'une autre? Peut-être que je pense trop à Javascript.


Réponses:


85

MongoDB ne prend pas en charge les clés avec un point , vous devrez donc prétraiter votre fichier JSON pour les supprimer / les remplacer avant de l'importer ou vous vous préparerez à toutes sortes de problèmes.

Il n'y a pas de solution de contournement standard à ce problème, la meilleure approche dépend trop des spécificités de la situation. Mais j'éviterais toute approche d'encodeur / décodeur de clé si possible, car vous continuerez à en payer les inconvénients à perpétuité, où une restructuration JSON serait vraisemblablement un coût unique.


1
Je ne pense pas qu'il existe une méthode standard, la meilleure approche dépend trop des spécificités de la situation. Mais j'éviterais toute approche codeur / décodeur clé si possible, car vous continuerez à en payer les inconvénients à perpétuité, où une restructuration JSON serait vraisemblablement un coût unique.
JohnnyHK

8
Ran dans cette situation à nouveau. Cela ne semble pas se produire tant avec les noms de clés d'application, que nous pouvons contrôler et sur lesquels nous devons souvent interroger, mais avec les données fournies par l'utilisateur dans des structures de données imbriquées, que nous ne pouvons pas contrôler, mais (a) aimerions stocker dans Mongo , (b) nous savons dans quels champs spécifiques cela peut arriver (par exemple modelsici), et (c) nous n'avons pas besoin de les interroger par nom de clé en Mongo. Donc, un modèle sur lequel je me suis installé est celui de JSON.stringifyce champ lors de la sauvegarde et de 'JSON.parse` lors de la récupération.
prototype

16
Si vous le devez, vous pouvez fournir l'option {check_keys: false} pour contourner ce problème.
Tzury Bar Yochay

5
@TzuryBarYochay OMG vous avez trouvé l'équivalent MongoDB du passage nord-ouest. Je pense que cela devrait être la réponse acceptée.
prototype

2
@emarel db.collection_foo.update ({this: "that"}, {$ set: {a: "b"}}, {check_keys: false})
Tzury Bar Yochay

22

Comme mentionné dans d'autres réponses, MongoDB n'autorise pas les caractères $ou .comme clés de carte en raison des restrictions sur les noms de champ . Cependant, comme mentionné dans Dollar Sign Operator Échapper à cette restriction ne vous empêche pas d' insérer des documents avec de telles clés, cela vous empêche simplement de les mettre à jour ou de les interroger.

Le problème du simple remplacement .par [dot]ou U+FF0E(comme mentionné ailleurs sur cette page) est le suivant: que se passe-t-il lorsque l'utilisateur souhaite légitimement stocker la clé [dot]ou U+FF0E?

Une approche adoptée par le pilote afMorphia de Fantom consiste à utiliser des séquences d'échappement unicode similaires à celles de Java, mais en veillant à ce que le caractère d'échappement soit d'abord échappé. En substance, les remplacements de chaînes suivants sont effectués (*):

\  -->  \\
$  -->  \u0024
.  -->  \u002e

Un remplacement inversé est effectué lorsque les clés de la carte sont ensuite lues à partir de MongoDB.

Ou en code Fantom :

Str encodeKey(Str key) {
    return key.replace("\\", "\\\\").replace("\$", "\\u0024").replace(".", "\\u002e")
}

Str decodeKey(Str key) {
    return key.replace("\\u002e", ".").replace("\\u0024", "\$").replace("\\\\", "\\")
}

Le seul moment où un utilisateur doit être conscient de ces conversions est lors de la construction de requêtes pour ces clés.

Étant donné qu'il est courant de stocker dotted.property.namesdans des bases de données à des fins de configuration, je pense que cette approche est préférable à l'interdiction simplement de toutes ces clés de carte.

(*) afMorphia exécute en fait des règles d'échappement Unicode complètes / appropriées comme mentionné dans la syntaxe d'échappement Unicode en Java, mais la séquence de remplacement décrite fonctionne tout aussi bien.


Devrait utiliser //gpour remplacer toutes les occurrences et pas seulement la première. De plus, utiliser les équivalents pleine largeur comme dans la réponse de Martin Konecny ​​semble être une bonne idée. Enfin, une barre oblique inverse suffit pour l'encodage. key.replace(/\./g, '\uff0e').replace(/\$/g, '\uff04').replace(/\\/g, '\uff3c')
cw

1
@cw '- Le code est dans une syntaxe de type Java, donc replace remplace en fait toutes les occurrences, et des doubles barres obliques inverses sont nécessaires pour échapper les barres obliques inverses. Et encore une fois, vous devez introduire une forme d'évasion pour vous assurer que tous les cas sont couverts. Quelqu'un, à un moment donné, peut en fait vouloir une clé de U+FF04.
Steve Eynon

2
En fait, Mongodb prend en charge les points et les dollars dans ses dernières versions. Voir: - stackoverflow.com/a/57106679/3515086
Abhidemon

18

La documentation Mongo suggère de remplacer les caractères illégaux tels que $et .par leurs équivalents Unicode.

Dans ces situations, les clés devront remplacer les $ et. personnages. N'importe quel caractère suffit, mais pensez à utiliser les équivalents pleine largeur Unicode: U + FF04 (c'est-à-dire «$») et U + FF0E (c'est-à-dire «.»).


74
Cela ressemble à une recette pour des maux de tête de débogage massifs sur la route.
personne le

2
@AndrewMedico, @tamlyn - Je pense que la documentation veut dire quelque chose commedb.test.insert({"field\uff0ename": "test"})
P. Myer Nore

4
-1 R. C'est une idée terrible - et si quelqu'un essayait réellement d'utiliser ces caractères Unicode comme clé? Ensuite, vous avez une erreur silencieuse qui fera qui sait quoi à votre système. N'utilisez pas de méthodes d'échappement ambiguës comme ça. B. la documentation mongo ne dit plus cela, probablement parce que quelqu'un a réalisé que c'était une idée terrible
BT

7
@SergioTulentsev Je leur ai demandé de supprimer la recommandation :) github.com/mongodb/docs/commit/…
BT

2
@BT: chapeau à vous, monsieur :)
Sergio Tulentsev

15

La dernière version stable (v3.6.1) de MongoDB prend désormais en charge les points (.) Dans les clés ou les noms de champ.

Les noms de champ peuvent désormais contenir des points (.) Et des caractères dollar ($)


10
Même si le serveur le prend maintenant en charge, le pilote vérifie toujours les $ et les points dans les clés et ne les accepte pas. Par conséquent, Mongo ne prend théoriquement en charge que les points et les caractères dollar. Pratiquement ce n'est pas encore utilisable :(
JMax

Peut-être que vous utilisez un client ancien ou incompatible. Je l'utilise sur mes serveurs de production sans aucune sueur. J'ai vérifié les clients NodeJS et Java.
h4ck3d

Avec Java, cela ne fonctionne définitivement pas! Essayez la commande suivante: mongoClient.getDatabase("mydb").getCollection("test").insertOne(new Document("value", new Document("key.with.dots", "value").append("$dollar", "value")));Cela échoue en utilisant mongodb-driver.3.6.3 et MongoDB 3.6.3.
JMax

1
En effet, je viens d'essayer avec une configuration mongodb-4.1.1et pymongo-3.7.1. Je peux ajouter des documents contenant des clés avec avec .avec robomongo mais pas à partir de pymongo, cela soulève encore le InvalidDocument: key '1.1' must not contain '.'souhait qu'il ait été corrigé maintenant ...
apprentissage est un désordre

J'ai essayé avec le serveur mongodb 4.0.9 et le pilote java 3.10.2 mais il n'accepte pas le point dans le nom de la clé. c'est étrange que lorsque vous essayez d'utiliser robomongo, cela fonctionne ...
xyzt

12

Une solution que je viens de mettre en œuvre et qui me satisfait vraiment consiste à diviser le nom et la valeur de la clé en deux champs distincts. De cette façon, je peux garder les personnages exactement les mêmes et ne pas m'inquiéter de ces cauchemars d'analyse. Le document ressemblerait à:

{
    ...
    keyName: "domain.com",
    keyValue: "unregistered",
    ...
}

Vous pouvez toujours interroger cela assez facilement, simplement en effectuant un findsur les champs keyName et keyValue .

Donc au lieu de:

 db.collection.find({"domain.com":"unregistered"})

qui ne fonctionnerait pas réellement comme prévu, vous exécuteriez:

db.collection.find({keyName:"domain.com", keyValue:"unregistered"})

et il renverra le document attendu.


Comment vous l'avez fait? Pourriez-vous s'il vous plaît m'aider avec ce même cas.
profiler

J'ai ajouté un exemple de requête. Est ce que ça aide?
Steve le

10

Vous pouvez essayer d'utiliser un hachage dans la clé au lieu de la valeur, puis stocker cette valeur dans la valeur JSON.

var crypto = require("crypto");   

function md5(value) {
    return crypto.createHash('md5').update( String(value) ).digest('hex');
}

var data = {
    "_id": {
        "$oid": "..."
    },
    "make": "saab",
    "models": {}
}

var version = "9.7x";

data.models[ md5(version) ] = {
    "version": version,
    "years" : [
        2007,
        2008,
        2009,
        2010
    ]
}

Vous accéderiez ensuite aux modèles en utilisant le hachage plus tard.

var version = "9.7x";
collection.find( { _id : ...}, function(e, data ) {
    var models = data.models[ md5(version) ];
}

1
J'aime ça, une solution propre avec un hachage unidirectionnel et très similaire à la façon dont les choses fonctionnent sous le capot.
Michael Yagudaev

3
Le problème avec l'utilisation des hachages comme clés, c'est qu'ils ne sont pas garantis pour être uniques et qu'ils produisent fréquemment des collisions . De plus, calculer un hachage cryptographique à chaque fois que vous souhaitez accéder à une carte ne me semble pas la solution la plus optimale.
Steve Eynon

2
Pourquoi est-ce mieux que de remplacer le point par un caractère spécial ou une séquence?
B Seven

La conversion de chaînes en base64 est bien meilleure.
Zen

8

Il est pris en charge maintenant

MongoDb 3.6 et les versions ultérieures prennent en charge les points et le dollar dans les noms de champ. Voir ci-dessous JIRA: https://jira.mongodb.org/browse/JAVA-2810

La mise à niveau de votre Mongodb vers 3.6+ semble être la meilleure solution.


C'est la meilleure réponse ici. : +1
hello_abhishek

3
3.6 peut les stocker, oui, mais il n'est pas encore pris en charge, peut générer des erreurs de pilote et peut interrompre les requêtes / mises à jour: restrictions : "Le langage de requête MongoDB ne peut pas toujours exprimer de manière significative des requêtes sur des documents dont les noms de champ contiennent ces caractères (voir SERVER- 30575). Tant que le support n'est pas ajouté dans le langage de requête, l'utilisation de $ et. Dans les noms de champ n'est pas recommandée et n'est pas prise en charge par les pilotes officiels MongoDB. "
JeremyDouglass

4

À partir de la documentation MongoDB, "le '.' le caractère ne doit apparaître nulle part dans le nom de la clé ". Il semble que vous deviez créer un schéma d'encodage ou vous en passer.


4

Vous devrez échapper aux clés. Comme il semble que la plupart des gens ne savent pas comment échapper correctement aux chaînes, voici les étapes:

  1. choisissez un caractère d'échappement (il est préférable de choisir un personnage rarement utilisé). Par exemple. '~'
  2. Pour échapper, remplacez d'abord toutes les instances du caractère d'échappement par une séquence précédée de votre caractère d'échappement (par exemple '~' -> '~ t'), puis remplacez le caractère ou la séquence dont vous avez besoin pour échapper par une séquence précédée de votre caractère d'échappement . Par exemple. '.' -> '~ p'
  3. Pour unscape, supprimez d'abord la séquence d'échappement de toutes les instances de votre deuxième séquence d'échappement (par exemple '~ p' -> '.'), Puis transformez votre séquence de caractères d'échappement en un seul caractère d'échappement (par exemple '~ s' -> '~ ')

Souvenez-vous également que mongo n'autorise pas non plus les clés à commencer par '$', vous devez donc faire quelque chose de similaire ici

Voici un code qui le fait:

// returns an escaped mongo key
exports.escape = function(key) {
  return key.replace(/~/g, '~s')
            .replace(/\./g, '~p')
            .replace(/^\$/g, '~d')
}

// returns an unescaped mongo key
exports.unescape = function(escapedKey) {
  return escapedKey.replace(/^~d/g, '$')
                   .replace(/~p/g, '.')
                   .replace(/~s/g, '~')
}

Cet échappement peut encore casser, si vous avez des chaînes comme '. ~ P.'. Ici, la chaîne échappée sera '~ p ~~ p ~ p'. Un échappée vous donnera «. ~ ..», qui est différent de la chaîne réelle.
jvc

1
@jvc Vous avez raison! J'ai corrigé les fonctions d'explication et d'exemple d'échappement. Faites-moi savoir s'ils sont toujours cassés!
BT

3

Une réponse tardive, mais si vous utilisez Spring et Mongo, Spring peut gérer la conversion pour vous avec MappingMongoConverter. C'est la solution de JohnnyHK mais gérée par Spring.

@Autowired
private MappingMongoConverter converter;

@PostConstruct
public void configureMongo() {
 converter.setMapKeyDotReplacement("xxx");
}

Si votre Json stocké est:

{ "axxxb" : "value" }

Grâce à Spring (MongoClient), il sera lu comme suit:

{ "a.b" : "value" }

requis un bean de type 'org.springframework.data.mongodb.core.convert.MappingMongoConverter' qui n'a pas pu être trouvé.
Sathya Narayan C

1

J'utilise l'échappement suivant en JavaScript pour chaque clé d'objet:

key.replace(/\\/g, '\\\\').replace(/^\$/, '\\$').replace(/\./g, '\\_')

Ce que j'aime, c'est qu'il ne remplace qu'au $début, et qu'il n'utilise pas de caractères unicode qui peuvent être difficiles à utiliser dans la console. _est pour moi beaucoup plus lisible qu'un caractère unicode. Il ne remplace pas non plus un jeu de caractères spéciaux ( $, .) par un autre (unicode). Mais s'échappe correctement avec le traditionnel \.


3
Et si quelqu'un utilise un _ dans l'une de ses clés, vous obtiendrez des bogues.
BT

1

Pas parfait, mais fonctionnera dans la plupart des situations: remplacez les caractères interdits par autre chose. Comme il s'agit de clés, ces nouveaux caractères devraient être assez rares.

/** This will replace \ with ⍀, ^$ with '₴' and dots with ⋅  to make the object compatible for mongoDB insert. 
Caveats:
    1. If you have any of ⍀, ₴ or ⋅ in your original documents, they will be converted to \$.upon decoding. 
    2. Recursive structures are always an issue. A cheap way to prevent a stack overflow is by limiting the number of levels. The default max level is 10.
 */
encodeMongoObj = function(o, level = 10) {
    var build = {}, key, newKey, value
    //if (typeof level === "undefined") level = 20     // default level if not provided
    for (key in o) {
        value = o[key]
        if (typeof value === "object") value = (level > 0) ? encodeMongoObj(value, level - 1) : null     // If this is an object, recurse if we can

        newKey = key.replace(/\\/g, '⍀').replace(/^\$/, '₴').replace(/\./g, '⋅')    // replace special chars prohibited in mongo keys
        build[newKey] = value
    }
    return build
}

/** This will decode an object encoded with the above function. We assume the structure is not recursive since it should come from Mongodb */
decodeMongoObj = function(o) {
    var build = {}, key, newKey, value
    for (key in o) {
        value = o[key]
        if (typeof value === "object") value = decodeMongoObj(value)     // If this is an object, recurse
        newKey = key.replace(/⍀/g, '\\').replace(/^₴/, '$').replace(/⋅/g, '.')    // replace special chars prohibited in mongo keys
        build[newKey] = value
    }
    return build
}

Voici un test:

var nastyObj = {
    "sub.obj" : {"$dollar\\backslash": "$\\.end$"}
}
nastyObj["$you.must.be.kidding"] = nastyObj     // make it recursive

var encoded = encodeMongoObj(nastyObj, 1)
console.log(encoded)
console.log( decodeMongoObj( encoded) )

et les résultats - notez que les valeurs ne sont pas modifiées:

{
  sub⋅obj: {
    ₴dollar⍀backslash: "$\\.end$"
  },
  ₴you⋅must⋅be⋅kidding: {
    sub⋅obj: null,
    ₴you⋅must⋅be⋅kidding: null
  }
}
[12:02:47.691] {
  "sub.obj": {
    $dollar\\backslash: "$\\.end$"
  },
  "$you.must.be.kidding": {
    "sub.obj": {},
    "$you.must.be.kidding": {}
  }
}

1

Il existe une manière moche d'interroger il n'est pas recommandé de l'utiliser dans l'application plutôt qu'à des fins de débogage (ne fonctionne que sur les objets intégrés):

db.getCollection('mycollection').aggregate([
    {$match: {mymapfield: {$type: "object" }}}, //filter objects with right field type
    {$project: {mymapfield: { $objectToArray: "$mymapfield" }}}, //"unwind" map to array of {k: key, v: value} objects
    {$match: {mymapfield: {k: "my.key.with.dot", v: "myvalue"}}} //query
])

1

Comme un autre utilisateur l'a mentionné, l'encodage / décodage peut devenir problématique à l'avenir, il est donc probablement plus facile de remplacer toutes les clés qui ont un point. Voici une fonction récursive que j'ai créée pour remplacer les clés par '.' occurrences:

def mongo_jsonify(dictionary):
    new_dict = {}
    if type(dictionary) is dict:
        for k, v in dictionary.items():
            new_k = k.replace('.', '-')
            if type(v) is dict:
                new_dict[new_k] = mongo_jsonify(v)
            elif type(v) is list:
                new_dict[new_k] = [mongo_jsonify(i) for i in v]
            else:
                new_dict[new_k] = dictionary[k]
        return new_dict
    else:
        return dictionary

if __name__ == '__main__':
    with open('path_to_json', "r") as input_file:
        d = json.load(input_file)
    d = mongo_jsonify(d)
    pprint(d)

Vous pouvez également modifier ce code pour remplacer '$', car c'est un autre caractère que mongo n'autorisera pas dans une clé.


0

Pour PHP, je remplace la valeur HTML par la période. C'est"." .

Il stocke dans MongoDB comme ceci:

  "validations" : {
     "4e25adbb1b0a55400e030000" : {
     "associate" : "true" 
    },
     "4e25adb11b0a55400e010000" : {
       "associate" : "true" 
     } 
   } 

et le code PHP ...

  $entry = array('associate' => $associate);         
  $data = array( '$set' => array( 'validations.' . str_replace(".", `"."`, $validation) => $entry ));     
  $newstatus = $collection->update($key, $data, $options);      

0

Les paires Lodash vous permettront de changer

{ 'connect.sid': 's:hyeIzKRdD9aucCc5NceYw5zhHN5vpFOp.0OUaA6' }

dans

[ [ 'connect.sid',
's:hyeIzKRdD9aucCc5NceYw5zhHN5vpFOp.0OUaA6' ] ]

en utilisant

var newObj = _.pairs(oldObj);

0

Vous pouvez le stocker tel quel et le convertir en joli après

J'ai écrit cet exemple sur Livescript. Vous pouvez utiliser le site Web livescript.net pour l'évaluer

test =
  field:
    field1: 1
    field2: 2
    field3: 5
    nested:
      more: 1
      moresdafasdf: 23423
  field3: 3



get-plain = (json, parent)->
  | typeof! json is \Object => json |> obj-to-pairs |> map -> get-plain it.1, [parent,it.0].filter(-> it?).join(\.)
  | _ => key: parent, value: json

test |> get-plain |> flatten |> map (-> [it.key, it.value]) |> pairs-to-obj

Cela produira

{"field.field1":1,
 "field.field2":2,
 "field.field3":5,
 "field.nested.more":1,
 "field.nested.moresdafasdf":23423,
 "field3":3}


0

Donnez-vous mon conseil: vous pouvez utiliser JSON.stringify pour enregistrer Object / Array contient le nom de la clé a des points, puis analyser la chaîne en Object avec JSON.parse à traiter lors de l'obtention des données de la base de données

Une autre solution de contournement: Restructurez votre schéma comme:

key : {
"keyName": "a.b"
"value": [Array]
}

0

Le dernier MongoDB prend en charge les clés avec un point, mais le pilote Java MongoDB ne prend pas en charge. Donc, pour le faire fonctionner en Java, j'ai extrait le code du repo github de java-mongo-driver et apporté des modifications en conséquence dans leur fonction isValid Key , en ai créé un nouveau fichier, en l'utilisant maintenant.


0

Remplacez le point ( .) ou le dollar ( $) par d'autres caractères qui ne seront jamais utilisés dans le document réel. Et restaurez le point ( .) ou le dollar ($ ) lors de la récupération du document. La stratégie n'influencera pas les données lues par l'utilisateur.

Vous pouvez sélectionner le personnage parmi tous les personnages .


0

Ce qui est étrange, c'est qu'en utilisant mongojs, je peux créer un document avec un point si je définis le _id moi-même, mais je ne peux pas créer de document lorsque le _id est généré:

Fonctionne:

db.testcollection.save({"_id": "testdocument", "dot.ted.": "value"}, (err, res) => {
    console.log(err, res);
});

Ne marche pas:

db.testcollection.save({"dot.ted": "value"}, (err, res) => {
    console.log(err, res);
});

J'ai d'abord pensé que la mise à jour d'un document avec une clé de point fonctionnait également, mais son identification du point comme sous-clé!

Voyant comment mongojs gère le point (sous-clé), je vais m'assurer que mes clés ne contiennent pas de point.


0

Comme ce que @JohnnyHK a mentionné, supprimez les ponctuations ou "." de vos clés car cela créera des problèmes beaucoup plus importants lorsque vos données commenceront à s'accumuler dans un ensemble de données plus grand. Cela posera des problèmes en particulier lorsque vous appelez des opérateurs d'agrégation comme $ merge, qui nécessitent d'accéder et de comparer des clés, ce qui générera une erreur. Je l'ai appris à la dure s'il vous plaît ne répétez pas pour ceux qui débutent.


-2

/home/user/anaconda3/lib/python3.6/site-packages/pymongo/collection.py

Je l'ai trouvé dans les messages d'erreur. Si vous utilisez anaconda(trouvez le fichier correspondant sinon), changez simplement la valeur de check_keys = Trueà Falsedans le fichier indiqué ci-dessus. Cela fonctionnera!

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.