Punto de MongoDB (.) En el nombre de la clave


94

Parece que mongo no permite la inserción de claves con un punto (.) O un signo de dólar ($); sin embargo, cuando importé un archivo JSON que contenía un punto usando la herramienta mongoimport, funcionó bien. El conductor se queja de intentar insertar ese elemento.

Así es como se ve el documento en la base de datos:

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

¿Estoy haciendo todo esto mal y no debería usar mapas hash como ese con datos externos (es decir, los modelos) o puedo escapar del punto de alguna manera? Tal vez estoy pensando demasiado en Javascript.


Respuestas:


85

MongoDB no admite claves con un punto , por lo que tendrá que preprocesar su archivo JSON para eliminarlo / reemplazarlo antes de importarlo o se estará preparando para todo tipo de problemas.

No existe una solución alternativa estándar para este problema, el mejor enfoque depende demasiado de las características específicas de la situación. Pero evitaría cualquier enfoque de codificador / decodificador de clave si es posible, ya que continuará pagando el inconveniente de eso a perpetuidad, donde una reestructuración de JSON presumiblemente sería un costo único.


1
No creo que haya una forma estándar, el mejor enfoque depende demasiado de los detalles de la situación. Pero evitaría cualquier enfoque de codificador / decodificador de clave si es posible, ya que continuará pagando el inconveniente de eso a perpetuidad, donde una reestructuración de JSON presumiblemente sería un costo único.
JohnnyHK

8
Me encontré con esta situación de nuevo. Esto parece ocurrir no tanto con los nombres de las claves de las aplicaciones, que podemos controlar y que a menudo necesitamos consultar, sino con los datos proporcionados por el usuario en estructuras de datos anidadas, que no podemos controlar, pero (a) nos gustaría almacenar en Mongo , (b) sabemos en qué campos específicos podría suceder esto (por ejemplo, modelsaquí), y (c) no necesitamos consultarlos por nombre de clave en Mongo. Entonces, un patrón en el que me decidí es en JSON.stringifyeste campo al guardar y 'JSON.parse` al recuperar.
prototipo

16
Si debe hacerlo, puede proporcionar la opción {check_keys: false} para evitar este problema.
Tzury Bar Yochay

5
@TzuryBarYochay Dios mío, has encontrado el equivalente en MongoDB del pasaje noroeste. Creo que esta debería ser la respuesta aceptada.
prototipo

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

22

Como se menciona en otras respuestas, MongoDB no permite $o .caracteres como claves de mapa debido a restricciones en los nombres de campo . Sin embargo, como se menciona en Operador de signo de dólar, escapar de esta restricción no le impide insertar documentos con dichas claves, solo le impide actualizarlos o consultarlos.

El problema de simplemente reemplazar .con [dot]o U+FF0E(como se mencionó en otra parte de esta página) es, ¿qué sucede cuando el usuario legítimamente quiere almacenar la clave [dot]o U+FF0E?

Un enfoque que adopta el controlador afMorphia de Fantom es usar secuencias de escape Unicode similares a las de Java, pero asegurándose de que el carácter de escape se escape primero. En esencia, se realizan los siguientes reemplazos de cadenas (*):

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

Se realiza un reemplazo inverso cuando las claves del mapa se leen posteriormente desde MongoDB.

O en código Fantom :

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

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

El único momento en que un usuario debe estar al tanto de tales conversiones es cuando construye consultas para tales claves.

Dado que es común almacenar dotted.property.namesen bases de datos con fines de configuración, creo que este enfoque es preferible a simplemente prohibir todas estas claves de mapa.

(*) afMorphia en realidad realiza reglas de escape Unicode completas / adecuadas como se menciona en la sintaxis de escape Unicode en Java, pero la secuencia de reemplazo descrita funciona igual de bien.


Debe usarse //gpara reemplazar todas las ocurrencias y no solo la primera. Además, usar los equivalentes de ancho completo como en la respuesta de Martin Konecny ​​parece ser una buena idea. Finalmente, una barra invertida es suficiente para la codificación. key.replace(/\./g, '\uff0e').replace(/\$/g, '\uff04').replace(/\\/g, '\uff3c')
cw '

1
@cw ': el código está en una sintaxis similar a Java, por lo que reemplazar realmente reemplaza todas las ocurrencias, y se requieren barras invertidas dobles para escapar de las barras invertidas. Y nuevamente, debe introducir alguna forma de escape para asegurarse de que todos los casos estén cubiertos. Alguien, en algún momento, puede querer una clave de U+FF04.
Steve Eynon

2
Resulta que Mongodb admite puntos y dólares en sus últimas versiones. Ver: - stackoverflow.com/a/57106679/3515086
Abhidemon

18

Los documentos de Mongo sugieren reemplazar caracteres ilegales como $y .por sus equivalentes Unicode.

En estas situaciones, las claves deberán sustituir el $ y. caracteres. Cualquier carácter es suficiente, pero considere usar los equivalentes de ancho completo Unicode: U + FF04 (es decir, “$”) y U + FF0E (es decir, “.”).


74
Eso suena como una receta para los dolores de cabeza masivos de depuración en el futuro.
nadie

2
@AndrewMedico, @tamlyn - Creo que los documentos significan algo comodb.test.insert({"field\uff0ename": "test"})
P. Myer Nore

4
-1 R. Esa es una idea terrible. ¿Qué pasa si alguien realmente está tratando de usar esos caracteres Unicode como clave? Entonces tienes un error silencioso que hará quién sabe qué en tu sistema. No use métodos de escape ambiguos como ese. B. los médicos de Mongo ya no dicen eso, probablemente porque alguien se dio cuenta de que es una idea terrible
BT

7
@SergioTulentsev Conseguí que eliminaran la recomendación :) github.com/mongodb/docs/commit/…
BT

2
@BT: sombrero para usted, señor :)
Sergio Tulentsev

15

La última versión estable (v3.6.1) de MongoDB ahora admite puntos (.) En las claves o nombres de campo.

Los nombres de campo ahora pueden contener puntos (.) Y caracteres de dólar ($)


10
Incluso si el servidor lo admite ahora, el controlador todavía busca $ y puntos en las claves y no los acepta. Por lo tanto, Mongo solo admite teóricamente puntos y caracteres de dólar. Prácticamente esto aún no se puede usar :(
JMax

Quizás estés usando algún cliente antiguo o incompatible. He estado usando esto en mis servidores de producción sin sudar. He comprobado si hay clientes de NodeJS y Java.
h4ck3d

¡Con Java definitivamente no funciona! Intente el siguiente comando: mongoClient.getDatabase("mydb").getCollection("test").insertOne(new Document("value", new Document("key.with.dots", "value").append("$dollar", "value")));falla al usar mongodb-driver.3.6.3 y MongoDB 3.6.3.
JMax

1
De hecho, lo intenté con una configuración mongodb-4.1.1y pymongo-3.7.1. Puedo agregar documentos que contengan claves con .robomongo pero no de pymongo, el umbral aumenta InvalidDocument: key '1.1' must not contain '.'Desearía que se hubiera solucionado a estas alturas ...
aprendizaje es un desastre

Intenté con el servidor mongodb 4.0.9 y el controlador java 3.10.2 pero no acepta el punto en el nombre de la clave. es extraño que cuando intente usar robomongo funcione ...
xyzt

12

Una solución que acabo de implementar y con la que estoy muy contento consiste en dividir el nombre y el valor de la clave en dos campos separados. De esta manera, puedo mantener los personajes exactamente iguales y no preocuparme por ninguna de esas pesadillas de análisis. El documento se vería así:

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

Aún puede consultar esto con bastante facilidad, simplemente haciendo un finden los campos keyName y keyValue .

Entonces en lugar de:

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

que en realidad no funcionaría como se esperaba, ejecutaría:

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

y devolverá el documento esperado.


¿Como lo hiciste? ¿Podría ayudarme con el mismo caso?
perfilador

Agregué un ejemplo de consulta. ¿Eso ayuda?
Steve

10

Puede intentar usar un hash en la clave en lugar del valor y luego almacenar ese valor en el valor 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
    ]
}

Luego accedería a los modelos usando el hash más tarde.

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

1
Me gusta esta solución limpia con hash de 1 vía y muy similar a la forma en que funcionan las cosas debajo del capó.
Michael Yagudaev

3
El problema con el uso de hash como claves es que no se garantiza que sean únicos y con frecuencia producen colisiones . Además, calcular un hash criptográfico cada vez que desee acceder a un mapa no me parece la solución más óptima.
Steve Eynon

2
¿Por qué es esto mejor que reemplazar el punto con un carácter o secuencia especial?
B Siete

Convertir cadenas a base64 es mucho mejor.
Zen

8

Es compatible ahora

MongoDb 3.6 en adelante admite puntos y dólares en los nombres de campo. Vea a continuación JIRA: https://jira.mongodb.org/browse/JAVA-2810

Actualizar su Mongodb a 3.6+ parece ser la mejor manera de hacerlo.


Esta es la mejor respuesta aquí. : +1
hello_abhishek

3
3.6 puede almacenarlos, sí, pero aún no es compatible, puede generar errores de controlador y puede romper la consulta / actualizaciones: restricciones : "El lenguaje de consulta MongoDB no siempre puede expresar de manera significativa consultas sobre documentos cuyos nombres de campo contienen estos caracteres (consulte SERVER- 30575). Hasta que se agregue soporte en el lenguaje de consulta, no se recomienda el uso de $ y. En los nombres de campo y no es compatible con los controladores oficiales de MongoDB ".
JeremyDouglass

4

De los documentos de MongoDB, "el '.' el carácter no debe aparecer en ninguna parte del nombre de la clave ". Parece que tendrás que idear un esquema de codificación o prescindir de él.


4

Necesitarás escapar de las llaves. Dado que parece que la mayoría de la gente no sabe cómo escapar correctamente de las cuerdas, estos son los pasos:

  1. elija un carácter de escape (lo mejor es elegir un carácter que rara vez se usa). P.ej. '~'
  2. Para escapar, primero reemplace todas las instancias del carácter de escape con alguna secuencia precedida por su carácter de escape (por ejemplo, '~' -> '~ t'), luego reemplace cualquier carácter o secuencia que necesite para escapar con alguna secuencia precedida por su carácter de escape . P.ej. '.' -> '~ p'
  3. Para dejar de escapar, primero elimine la secuencia de escape de todas las instancias de su segunda secuencia de escape (por ejemplo, '~ p' -> '.'), Luego transforme su secuencia de caracteres de escape en un solo carácter de escape (por ejemplo, '~ s' -> '~ ')

Además, recuerde que mongo tampoco permite que las teclas comiencen con '$', por lo que debe hacer algo similar allí

Aquí hay un código que lo hace:

// 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, '~')
}

Este escape aún puede romperse, si tiene cadenas como '. ~ P.'. Aquí la cadena de escape será '~ p ~~ p ~ p'. El Unescaping te dará '. ~ ..', que es diferente de la cadena real.
jvc

1
@jvc ¡Tienes razón! He arreglado la explicación y el ejemplo de las funciones de escape. ¡Avísame si todavía están rotos!
BT

3

Una respuesta tardía, pero si usa Spring y Mongo, Spring puede administrar la conversión por usted con MappingMongoConverter. Es la solución de JohnnyHK pero manejada por Spring.

@Autowired
private MappingMongoConverter converter;

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

Si su Json almacenado es:

{ "axxxb" : "value" }

A través de Spring (MongoClient) se leerá como:

{ "a.b" : "value" }

requería un bean de tipo 'org.springframework.data.mongodb.core.convert.MappingMongoConverter' que no se pudo encontrar.
Sathya Narayan C

1

Utilizo el siguiente escape en JavaScript para cada clave de objeto:

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

Lo que me gusta de él es que reemplaza solo $al principio y no usa caracteres Unicode que pueden ser difíciles de usar en la consola. _es para mí mucho más legible que un carácter Unicode. Tampoco reemplaza un conjunto de caracteres especiales ( $, .) con otro (unicode). Pero se escapa propiamente con lo tradicional \.


3
Y si alguien usa un _ en cualquiera de sus claves, obtendrá errores.
BT

1

No es perfecto, pero funcionará en la mayoría de situaciones: reemplace los caracteres prohibidos por otra cosa. Dado que está en las claves, estos nuevos caracteres deberían ser bastante raros.

/** 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
}

Aquí hay una prueba:

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) )

y los resultados - tenga en cuenta que los valores no se modifican:

{
  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

Hay una forma desagradable de consultar, no se recomienda usarla en la aplicación en lugar de con fines de depuración (funciona solo en objetos incrustados):

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

Como mencionó otro usuario, codificar / decodificar esto puede volverse problemático en el futuro, por lo que probablemente sea más fácil reemplazar todas las claves que tienen un punto. Aquí hay una función recursiva que hice para reemplazar las teclas con '.' ocurrencias:

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)

También puede modificar este código para reemplazar '$', ya que ese es otro carácter que mongo no permitirá en una clave.


0

Para PHP, sustituyo el valor HTML por el período. Eso es ".".

Se almacena en MongoDB así:

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

y el código PHP ...

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

0

Los pares de Lodash te permitirán cambiar

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

dentro

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

utilizando

var newObj = _.pairs(oldObj);

0

Puede almacenarlo como está y convertirlo en bonito después

Escribí este ejemplo en Livescript. Puede usar el sitio web livescript.net para evaluarlo

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

Producirá

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


0

Darle mi consejo: puede usar JSON.stringify para guardar Object / Array contiene el nombre de la clave tiene puntos, luego analizar la cadena a Object con JSON.parse para procesar cuando obtenga datos de la base de datos

Otra solución alternativa: reestructura tu esquema como:

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


0

Reemplace el punto ( .) o el dólar ( $) con otros caracteres que nunca se usarán en el documento real. Y restaure el punto ( .) o el dólar ( $) al recuperar el documento. La estrategia no influirá en los datos que lea el usuario.

Puede seleccionar el personaje de todos los personajes .


0

Lo extraño es que, usando mongojs, puedo crear un documento con un punto si configuro el _id yo mismo, sin embargo, no puedo crear un documento cuando se genera el _id:

Funciona:

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

No funciona:

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

Primero pensé que actualizar un documento con una clave de punto también funcionaba, ¡pero es identificar el punto como una subclave!

Al ver cómo mongojs maneja el punto (subclave), me aseguraré de que mis claves no contengan un punto.


0

Como lo que ha mencionado @JohnnyHK , elimine los signos de puntuación o '.' de sus claves porque creará problemas mucho mayores cuando sus datos comiencen a acumularse en un conjunto de datos más grande. Esto causará problemas, especialmente cuando llame a operadores agregados como $ merge, que requiere acceder y comparar claves que arrojarán un error. Lo he aprendido de la manera difícil, por favor no repita para los que están comenzando.


-2

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

Lo encontré en mensajes de error. Si usa anaconda(busque el archivo correspondiente si no es así), simplemente cambie el valor de check_keys = Truea Falseen el archivo indicado arriba. ¡Eso funcionará!

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.