Ponto MongoDB (.) No nome da chave

96

Parece que o mongo não permite a inserção de chaves com um ponto (.) Ou cifrão ($), mas quando importei um arquivo JSON que continha um ponto usando a ferramenta mongoimport, funcionou bem. O motorista está reclamando de tentar inserir aquele elemento.

Esta é a aparência do documento no banco de dados:

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

Estou fazendo tudo errado e não deveria usar mapas hash como esse com dados externos (ou seja, os modelos) ou posso escapar do ponto de alguma forma? Talvez eu esteja pensando demais no estilo Javascript.

Michael Yagudaev
fonte
Vale a pena dar uma olhada em npmjs.com/package/mongo-escape
Sam Denty

Respostas:

87

O MongoDB não oferece suporte a chaves com um ponto , então você terá que pré-processar seu arquivo JSON para removê-los / substituí-los antes de importá-lo ou estará se preparando para todos os tipos de problemas.

Não há uma solução alternativa padrão para esse problema, a melhor abordagem depende muito das especificidades da situação. Mas eu evitaria qualquer abordagem de codificador / decodificador de chave, se possível, já que você continuará a pagar pela inconveniência disso perpetuamente, onde uma reestruturação JSON seria presumivelmente um custo único.

JohnnyHK
fonte
1
Não acho que haja uma maneira padrão, a melhor abordagem depende muito das especificidades da situação. Mas eu evitaria qualquer abordagem de codificador / decodificador de chave, se possível, pois você continuará a pagar pela inconveniência disso perpetuamente, onde uma reestruturação JSON seria presumivelmente um custo único.
JohnnyHK
8
Encontrei esta situação novamente. Isso parece ocorrer não tanto com nomes de chaves de aplicativos, que podemos controlar e frequentemente precisamos consultar, mas com dados fornecidos pelo usuário em estruturas de dados aninhadas, que não podemos controlar, mas (a) gostaríamos de armazenar no Mongo , (b) sabemos em quais campos específicos isso pode acontecer (por exemplo, modelsaqui) e (c) não precisamos consultá-los por nome de chave no Mongo. Portanto, um padrão que estabeleci é JSON.stringifyeste campo ao salvar e 'JSON.parse` ao recuperar.
protótipo de
16
Se necessário, você pode fornecer a opção {check_keys: false} para contornar esse problema.
Tzury Bar Yochay
5
@TzuryBarYochay OMG, você encontrou o equivalente no MongoDB da passagem noroeste. Acho que essa deve ser a resposta aceita.
protótipo de
2
@emarel db.collection_foo.update ({this: "that"}, {$ set: {a: "b"}}, {check_keys: false})
Tzury Bar Yochay
23

Conforme mencionado em outras respostas, o MongoDB não permite caracteres $ou .como chaves de mapa devido a restrições nos nomes de campo . No entanto, conforme mencionado em Dollar Sign Operator Escaping esta restrição não o impede de inserir documentos com tais chaves, apenas impede que você os atualize ou consulte.

O problema de simplesmente substituir .por [dot]ou U+FF0E(conforme mencionado em outra parte desta página) é, o que acontece quando o usuário deseja legitimamente armazenar a chave [dot]ou U+FF0E?

Uma abordagem que o driver afMorphia do Fantom adota é usar sequências de escape Unicode semelhantes às do Java, mas garantindo que o caractere de escape seja escapado primeiro. Em essência, as seguintes substituições de string são feitas (*):

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

Uma substituição reversa é feita quando as chaves do mapa são lidas subsequentemente no MongoDB.

Ou no 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("\\\\", "\\")
}

O único momento em que o usuário precisa estar ciente dessas conversões é ao construir consultas para essas chaves.

Visto que é comum armazenar dotted.property.namesem bancos de dados para fins de configuração, acredito que essa abordagem seja preferível a simplesmente banir todas essas chaves de mapa.

(*) afMorphia realmente executa regras de escape unicode completas / adequadas, conforme mencionado na sintaxe de escape Unicode em Java, mas a sequência de substituição descrita funciona da mesma forma.

Steve Eynon
fonte
Deve ser usado //gpara substituir todas as ocorrências e não apenas a primeira. Além disso, usar os equivalentes de largura total como na resposta de Martin Konecny ​​parece ser uma boa ideia. Finalmente, uma barra invertida é suficiente para a codificação. key.replace(/\./g, '\uff0e').replace(/\$/g, '\uff04').replace(/\\/g, '\uff3c')
cw '
1
@cw '- O código está em uma sintaxe semelhante ao Java, portanto, substituir realmente substitui todas as ocorrências, e barras invertidas duplas são necessárias para escapar das barras invertidas. E, novamente, você precisa introduzir alguma forma de escape para garantir que todos os casos sejam cobertos. Alguém, em algum momento, pode realmente querer uma chave de U+FF04.
Steve Eynon
2
Acontece que Mongodb suporta pontos e dólares em suas versões mais recentes. Consulte: - stackoverflow.com/a/57106679/3515086
Abhidemon
18

Os documentos do Mongo sugerem a substituição de caracteres ilegais, como $e .por seus equivalentes Unicode.

Nessas situações, as chaves precisarão substituir o $ e reservado. personagens. Qualquer caractere é suficiente, mas considere usar os equivalentes de largura total Unicode: U + FF04 (ou seja, “$”) e U + FF0E (ou seja, “.”).

Martin Konecny
fonte
74
Isso soa como uma receita para grandes dores de cabeça de depuração no futuro.
ninguém
2
@AndrewMedico, @tamlyn - acho que os documentos significam algo comodb.test.insert({"field\uff0ename": "test"})
P. Myer Nore
4
-1 A. É uma ideia terrível - e se alguém estiver realmente tentando usar esses caracteres Unicode como uma chave? Então você tem um erro silencioso que fará quem sabe o que ao seu sistema. Não use métodos de escape ambíguos como esse. B. os médicos mongo não dizem mais isso, provavelmente porque alguém percebeu que é uma ideia terrível
BT
7
@SergioTulentsev Pedi que removessem a recomendação :) github.com/mongodb/docs/commit/…
BT
2
@BT: gorjeta para você, senhor :)
Sergio Tulentsev
15

A última versão estável (v3.6.1) do MongoDB agora oferece suporte a pontos (.) Nas chaves ou nomes de campo.

Os nomes dos campos podem conter caracteres de pontos (.) E cifrões ($) agora

h4ck3d
fonte
10
Mesmo que o servidor suporte agora, o driver ainda verifica se há $ e pontos nas chaves e não os aceita. Portanto, o Mongo só suporta teoricamente pontos e caracteres de dólar. Praticamente ainda não pode ser usado :(
JMax
Talvez você esteja usando algum cliente antigo ou incompatível. Tenho usado isso em meus servidores de produção sem nenhum problema. Verifiquei se há clientes NodeJS e Java.
h4ck3d
Com Java definitivamente não funciona! Tente o seguinte comando: mongoClient.getDatabase("mydb").getCollection("test").insertOne(new Document("value", new Document("key.with.dots", "value").append("$dollar", "value")));Falha ao usar mongodb-driver.3.6.3 e MongoDB 3.6.3.
JMax
1
Na verdade, eu apenas tentei com uma configuração mongodb-4.1.1e pymongo-3.7.1. Posso adicionar documentos contendo chaves com o .robomongo, mas não do pymongo, ele ainda levanta o InvalidDocument: key '1.1' must not contain '.'desejo de que já tivesse sido consertado ...
aprendizado é uma bagunça
Tentei com o servidor mongodb 4.0.9 e o driver java 3.10.2, mas não aceita ponto no nome da chave. é estranho que ao tentar usar o robomongo ele funciona ...
xyzt
12

Uma solução que acabei de implementar e com a qual estou muito feliz envolve a divisão do nome e valor da chave em dois campos separados. Assim, posso manter os caracteres exatamente iguais e não me preocupar com nenhum desses pesadelos de análise. O documento seria assim:

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

Você ainda pode consultar isso facilmente, apenas fazendo um findnos campos keyName e keyValue .

Então, em vez de:

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

que não funcionasse como esperado, você executaria:

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

e ele retornará o documento esperado.

Steve
fonte
Como você fez isso? Você poderia me ajudar com o mesmo caso.
profiler
Eu adicionei um exemplo de consulta. Isso ajuda?
Steve
10

Você pode tentar usar um hash na chave em vez do valor e, em seguida, armazenar esse valor no 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
    ]
}

Você acessaria os modelos usando o hash posteriormente.

var version = "9.7x";
collection.find( { _id : ...}, function(e, data ) {
    var models = data.models[ md5(version) ];
}
Henry
fonte
1
Eu gosto dessa solução limpa com hash unilateral e muito semelhante à maneira como as coisas funcionam nos bastidores.
Michael Yagudaev
3
O problema de usar hashes como chaves é que não há garantia de que sejam exclusivos e frequentemente produzem colisões . Além disso, computar um hash criptográfico toda vez que você deseja acessar um mapa não parece a solução ideal para mim.
Steve Eynon
2
Por que isso é melhor do que substituir o ponto final por um caractere ou sequência especial?
B Sete
Converter strings em base64 é muito melhor.
Zen
8

É suportado agora

MongoDb 3.6 em diante suporta pontos e cifrões em nomes de campo. Veja abaixo JIRA: https://jira.mongodb.org/browse/JAVA-2810

Atualizar seu Mongodb para 3.6+ parece ser a melhor maneira de fazer.

Abhidemon
fonte
Esta é a melhor resposta aqui. : +1
hello_abhishek
3
3.6 pode armazená-los, sim, mas ainda não é suportado, pode lançar erros de driver e pode interromper a consulta / atualizações: restrições : "A linguagem de consulta MongoDB nem sempre pode expressar consultas de forma significativa sobre documentos cujos nomes de campo contenham esses caracteres (consulte SERVER- 30575). Até que o suporte seja adicionado na linguagem de consulta, o uso de $ e. Em nomes de campo não é recomendado e não é suportado pelos drivers oficiais do MongoDB. "
JeremyDouglass
4

Dos documentos do MongoDB "o '.' caractere não deve aparecer em nenhum lugar no nome da chave ". Parece que você terá que criar um esquema de codificação ou ficar sem ele.

matemática
fonte
4

Você precisará escapar das chaves. Uma vez que parece que a maioria das pessoas não sabe como escapar adequadamente das strings, aqui estão as etapas:

  1. escolha um caractere de escape (melhor escolher um caractere raramente usado). Por exemplo. '~'
  2. Para escapar, primeiro substitua todas as instâncias do caractere de escape por alguma sequência prefixada com seu caractere de escape (por exemplo, '~' -> '~ t'), então substitua qualquer caractere ou sequência que você precisa escapar por alguma sequência prefixada com seu caractere de escape . Por exemplo. '.' -> '~ p'
  3. Para cancelar o escape, primeiro remova a sequência de escape de todas as instâncias de sua segunda sequência de escape (por exemplo, '~ p' -> '.') E, em seguida, transforme sua sequência de caracteres de escape em um único caractere de escape (por exemplo, '~ s' -> '~ ')

Além disso, lembre-se de que o mongo também não permite que as teclas comecem com '$', então você deve fazer algo semelhante aqui

Aqui está um código que faz isso:

// 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, '~')
}
BT
fonte
Este escape ainda pode ser interrompido, se você tiver strings como '. ~ P.'. Aqui, a string com escape será '~ p ~~ p ~ p'. Sem escape lhe dará '. ~ ..', que é diferente da string real.
jvc de
1
@jvc Você está certo! Corrigi a explicação e as funções de escape de exemplo. Me avise se ainda estiverem quebrados!
BT
3

Uma resposta tardia, mas se você usar Spring e Mongo, Spring pode gerenciar a conversão para você com MappingMongoConverter. É a solução da JohnnyHK, mas tratada pela Spring.

@Autowired
private MappingMongoConverter converter;

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

Se o Json armazenado for:

{ "axxxb" : "value" }

Por meio do Spring (MongoClient), ele será lido como:

{ "a.b" : "value" }
PomPom
fonte
necessário um bean do tipo 'org.springframework.data.mongodb.core.convert.MappingMongoConverter' que não pôde ser encontrado.
Sathya Narayan C
1

Eu uso o seguinte escape em JavaScript para cada chave de objeto:

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

O que eu gosto nele é que ele substitui apenas $no início e não usa caracteres Unicode, que podem ser difíceis de usar no console. _é para mim muito mais legível do que um caractere Unicode. Também não substitui um conjunto de caracteres especiais ( $, .) por outro (Unicode). Mas escapa corretamente com o tradicional \.

Mitar
fonte
3
E se alguém usar _ em qualquer uma de suas chaves, você obterá bugs.
BT
1

Não é perfeito, mas funcionará na maioria das situações: substitua os caracteres proibidos por outra coisa. Como está nas chaves, esses novos caracteres devem 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
}

Aqui está um teste:

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

e os resultados - observe que os valores não são modificados:

{
  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": {}
  }
}
Nico
fonte
1

Existe uma maneira feia de consultar, não recomendado para usá-lo no aplicativo em vez de para fins de depuração (funciona apenas em objetos incorporados):

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
])
Sredni
fonte
1

Como outro usuário mencionou, codificar / decodificar isso pode se tornar problemático no futuro, então provavelmente é mais fácil substituir todas as chaves que têm um ponto. Aqui está uma função recursiva que criei para substituir as chaves por '.' ocorrências:

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)

Você pode modificar este código para substituir '$' também, já que é outro caractere que o mongo não permite em uma chave.

Teddy Haley
fonte
0

Para PHP, substituo o valor HTML do período. Isso é ".".

Ele armazena no MongoDB assim:

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

e o código PHP ...

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

Os pares Lodash permitirão que você mude

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

para dentro

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

usando

var newObj = _.pairs(oldObj);
movido a vapor
fonte
0

Você pode armazená-lo como está e converter em bonito depois

Escrevi este exemplo no Livescript. Você pode usar o site livescript.net para avaliá-lo

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

Vai produzir

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

Andrey Stehno
fonte
0

Dou minha dica: você pode usar JSON.stringify para salvar Object / Array contém o nome da chave com pontos e, em seguida, analisar string para Object com JSON.parse para processar quando obter dados do banco de dados

Outra solução alternativa: reestruture seu esquema como:

key : {
"keyName": "a.b"
"value": [Array]
}
Mr.Cra
fonte
0

O MongoDB mais recente oferece suporte a chaves com um ponto, mas o driver Java MongoDB não é compatível. Então, para fazê-lo funcionar em Java, extraí o código do github repo do java-mongo-driver e fiz as alterações correspondentes na função isValid Key, criei um novo jar a partir dele, usando-o agora.

ANDY MURAY
fonte
0

Substitua o ponto ( .) ou o dólar ( $) por outros caracteres que nunca serão usados ​​no documento real. E restaure o ponto ( .) ou o dólar ( $) ao recuperar o documento. A estratégia não influenciará os dados que o usuário lê.

Você pode selecionar o personagem de todos os personagens .

Simin Jie
fonte
0

O estranho é que, usando mongojs, posso criar um documento com um ponto se definir o _id sozinho, no entanto, não posso criar um documento quando o _id é gerado:

Funciona:

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

Não funciona:

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

A princípio pensei que atualizar um documento com uma chave de ponto também funcionaria, mas é identificar o ponto como uma subchave!

Vendo como mongojs lida com o ponto (subchave), vou garantir que minhas chaves não contenham um ponto.

Sam
fonte
0

Como o que @JohnnyHK mencionou, remova pontuações ou '.' de suas chaves porque criará problemas muito maiores quando seus dados começarem a se acumular em um conjunto de dados maior. Isso causará problemas, especialmente quando você chamar operadores agregados como $ merge, que requer o acesso e a comparação de chaves, o que gerará um erro. Eu aprendi da maneira mais difícil, por favor, não repita para aqueles que estão começando.

Yi Xiang Chong
fonte
-2

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

Encontrado em mensagens de erro. Se você usar anaconda(encontre o arquivo correspondente, se não), simplesmente altere o valor de check_keys = Truepara Falseno arquivo indicado acima. Isso vai funcionar!

Layang
fonte