Точка MongoDB (.) В имени ключа


94

Кажется, что mongo не позволяет вставлять ключи с точкой (.) Или знаком доллара ($), однако, когда я импортировал файл JSON, содержащий точку, с помощью инструмента mongoimport, он работал нормально. Драйвер жалуется на попытку вставить этот элемент.

Вот как выглядит документ в базе данных:

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

Я все делаю неправильно и не должен использовать подобные хэш-карты с внешними данными (т.е. моделями), или я могу как-то избежать точки? Может, я слишком много думаю о Javascript.


Стоит посмотреть, npmjs.com/package/mongo-escape
Сэм Денти

Ответы:


85

MongoDB не поддерживает ключи с точкой в них, поэтому вам придется предварительно обработать файл JSON, чтобы удалить / заменить их перед импортом, иначе вы будете настраивать себя для всех видов проблем.

Стандартного решения этой проблемы нет, лучший подход слишком зависит от специфики ситуации. Но я бы по возможности избегал использования какого-либо ключевого кодировщика / декодера, поскольку вы будете продолжать платить за неудобства, связанные с этим, на неограниченный срок, когда реструктуризация JSON, вероятно, будет единовременной платой.


1
Я не думаю, что есть стандартный способ, лучший подход слишком зависит от специфики ситуации. Но я бы по возможности избегал использования какого-либо ключевого кодировщика / декодера, поскольку вы будете продолжать платить за неудобства, связанные с этим, на неограниченный срок, когда реструктуризация JSON, вероятно, будет единовременной платой.
JohnnyHK

8
Снова столкнулся с этой ситуацией. Похоже, это происходит не столько с именами ключей приложений, которые мы можем контролировать и которые часто нуждаются в запросе, сколько с данными, предоставленными пользователем во вложенных структурах данных, которые мы не можем контролировать, но (а) хотели бы сохранить в Mongo , (б) мы знаем, в каких конкретных полях это может произойти (например, modelsздесь), и (в) нам не нужно запрашивать их по имени ключа в Mongo. Итак, шаблон, который я JSON.stringifyвыбрал, - это это поле при сохранении и JSON.parse при извлечении.
прототип

16
При необходимости вы можете указать параметр {check_keys: false}, чтобы обойти эту проблему.
Tzury Bar Yochay

5
@TzuryBarYochay OMG, вы нашли в MongoDB эквивалент северо-западного прохода. Я думаю, это должен быть принятый ответ.
прототип

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

22

Как упоминалось в других ответах, MongoDB не разрешает символы $или в .качестве ключей карты из-за ограничений на имена полей . Однако, как упоминалось в разделе «Оператор знака доллара», выход из этого ограничения не мешает вам вставлять документы с такими ключами, он просто не дает вам обновлять или запрашивать их.

Проблема простой замены .на [dot]или U+FF0E(как упоминалось в другом месте на этой странице) заключается в том, что происходит, когда пользователь законно хочет сохранить ключ [dot]или U+FF0E?

Подход, который использует драйвер afMorphia от Fantom , заключается в использовании escape-последовательностей Unicode, аналогичных Java, но с обеспечением экранирования сначала escape-символа. По сути делаются следующие замены строк (*):

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

Обратная замена выполняется, когда ключи карты впоследствии считываются из MongoDB.

Или в коде Fantom :

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

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

Единственный раз, когда пользователю нужно знать о таких преобразованиях, - это при построении запросов для таких ключей.

Учитывая, что dotted.property.namesв базах данных принято хранить данные для целей настройки, я считаю, что этот подход предпочтительнее простого запрета всех таких ключей карты.

(*) afMorphia фактически выполняет полные / правильные правила экранирования Unicode, как указано в синтаксисе экранирования Unicode в Java, но описанная последовательность замены работает так же хорошо.


Следует использовать //gдля замены всех вхождений, а не только первого. Кроме того, использование эквивалентов полной ширины, как в ответе Мартина Конечни, кажется хорошей идеей. Наконец, для кодирования достаточно одной обратной косой черты. key.replace(/\./g, '\uff0e').replace(/\$/g, '\uff04').replace(/\\/g, '\uff3c')
cw

1
@cw '- Код имеет синтаксис, подобный Java, поэтому replace действительно заменяет все вхождения, а для экранирования обратных косых черт требуются двойные обратные косые черты. И снова вам нужно ввести некоторую форму экранирования, чтобы обеспечить охват всех случаев. Кто-то когда-нибудь может действительно захотеть получить ключ от U+FF04.
Стив Эйнон

2
Как оказалось, Mongodb поддерживает точки и доллары в последних версиях. См .: - stackoverflow.com/a/57106679/3515086
Abhidemon

18

Документы Mongo предлагают заменить недопустимые символы, такие как $и, .их эквивалентами в Юникоде.

В этих ситуациях ключи нужно будет заменить зарезервированными $ и. символы. Достаточно любого символа, но рассмотрите возможность использования эквивалентов полной ширины Unicode: U + FF04 (т.е. «$») и U + FF0E (например, «.»).


74
Похоже, это рецепт для серьезных проблем с отладкой в ​​будущем.
никто

2
@AndrewMedico, @tamlyn - я думаю, что документы означают что-то вродеdb.test.insert({"field\uff0ename": "test"})
П. Майер Нор

4
-1 A. Это ужасная идея - что, если кто-то на самом деле пытается использовать эти символы Unicode в качестве ключа? Тогда у вас будет тихая ошибка, которая сделает, кто знает, что с вашей системой. Не используйте такие неоднозначные методы выхода. Б. Документы монго больше не говорят об этом, вероятно, потому, что кто-то осознал его ужасную идею
BT

7
@SergioTulentsev Я заставил их удалить рекомендацию :) github.com/mongodb/docs/commit/…
BT

2
@BT: шляпа перед вами, сэр :)
Серджио Тюленцев

15

Последняя стабильная версия (v3.6.1) MongoDB теперь поддерживает точки (.) В ключах или именах полей.

Теперь имена полей могут содержать символы точек (.) И доллара ($).


10
Даже если сервер поддерживает это сейчас, драйвер все равно проверяет наличие $ и точек в ключах и не принимает их. Поэтому Mongo только теоретически поддерживает точки и символы доллара. Практически это еще не работает :(
JMax

Возможно вы используете какой-то старый или несовместимый клиент. Я без труда использую это на своих производственных серверах. Я проверил клиентов NodeJS и Java.
h4ck3d

С Java определенно не работает! Попробуйте следующую команду: mongoClient.getDatabase("mydb").getCollection("test").insertOne(new Document("value", new Document("key.with.dots", "value").append("$dollar", "value")));Ошибка при использовании mongodb-driver.3.6.3 и MongoDB 3.6.3.
JMax

1
Действительно, я просто попробовал с настройкой mongodb-4.1.1и pymongo-3.7.1. Я могу добавлять документы, содержащие ключи, с .помощью robomongo, но не from pymongo, это порождает InvalidDocument: key '1.1' must not contain '.'желание, чтобы это было исправлено к настоящему времени ...
Обучение - это беспорядок

Я пробовал использовать сервер mongodb 4.0.9 и драйвер java 3.10.2, но он не принимает точку в имени ключа. странно, что при использовании robomongo это работает ...
xyzt

12

Только что реализованное мной решение, которым я действительно доволен, включает разделение имени и значения ключа на два отдельных поля. Таким образом, я могу сохранить персонажей в точности такими же и не беспокоиться ни о каких кошмарах. Документ будет выглядеть так:

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

Вы все еще можете запросить это достаточно просто, просто выполнив a findв полях keyName и keyValue .

Так что вместо:

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

что на самом деле не будет работать должным образом, вы запустите:

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

и он вернет ожидаемый документ.


Как ты это сделал? Не могли бы вы помочь мне с тем же случаем.
профайлер

Я добавил пример запроса. Это помогает?
Стив

10

Вы можете попробовать использовать хэш в ключе вместо значения, а затем сохранить это значение в значении 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
    ]
}

Затем вы получите доступ к моделям, используя хэш позже.

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

1
Мне нравится это чистое решение с односторонним хешированием, действительно похожее на то, как все работает под капотом.
Михаил Ягудаев

3
Проблема с использованием хешей в качестве ключей заключается в том, что их уникальность не гарантируется, и они часто вызывают конфликты . Кроме того, вычисление криптографического хеша каждый раз, когда вы хотите получить доступ к карте, мне не кажется самым оптимальным решением.
Стив Эйнон

2
Почему это лучше, чем замена точки на специальный символ или последовательность?
B Seven,

Преобразование строк в base64 намного лучше.
Zen

8

Поддерживается сейчас

MongoDb 3.6 и более поздних версий поддерживает как точки, так и доллар в именах полей. См. Ниже JIRA: https://jira.mongodb.org/browse/JAVA-2810

Обновление Mongodb до версии 3.6+ - лучший вариант.


Это лучший ответ здесь. : +1
hello_abhishek

3
3.6 может хранить их, да, но он еще не поддерживается, может вызывать ошибки драйвера и может нарушать запросы / обновления: ограничения : «Язык запросов MongoDB не всегда может осмысленно выражать запросы к документам, имена полей которых содержат эти символы (см. SERVER- 30575). Пока поддержка не будет добавлена ​​в язык запросов, использование $ и. В именах полей не рекомендуется и не поддерживается официальными драйверами MongoDB. "
JeremyDouglass


4

Вам нужно будет сбежать от ключей. Поскольку кажется, что большинство людей не знают, как правильно экранировать строки, вот шаги:

  1. выберите escape-символ (лучше всего выбрать редко используемый персонаж). Например. '~'
  2. Чтобы избежать, сначала замените все экземпляры escape-символа некоторой последовательностью с добавлением вашего escape-символа (например, '~' -> '~ t'), затем замените любой символ или последовательность, которые вам нужно экранировать, некоторой последовательностью, к которой добавлен ваш escape-символ. . Например. '.' -> '~ p'
  3. Чтобы отключить экранирование, сначала удалите escape-последовательность из всех экземпляров вашей второй escape-последовательности (например, '~ p' -> '.'), Затем преобразуйте последовательность escape-символов в один escape-символ (например, '~ s' -> '~ ')

Также помните, что mongo также не позволяет клавишам начинаться с '$', поэтому вам нужно сделать что-то подобное там

Вот код, который это делает:

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

Это экранирование все еще может сломаться, если у вас есть строки вроде '. ~ P.'. Здесь экранированная строка будет '~ p ~~ p ~ p'. При отмене экранирования вы получите '. ~ ..', который отличается от фактической строки.
СП

1
@jvc Вы правы! Я исправил объяснение и примеры escape-функций. Сообщите мне, если они все еще сломаны!
BT

3

Поздний ответ, но если вы используете Spring и Mongo, Spring может управлять преобразованием за вас с помощью MappingMongoConverter. Это решение JohnnyHK, но обработанное Spring.

@Autowired
private MappingMongoConverter converter;

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

Если ваш сохраненный Json:

{ "axxxb" : "value" }

Через Spring (MongoClient) это будет читаться как:

{ "a.b" : "value" }

требуется bean-объект типа org.springframework.data.mongodb.core.convert.MappingMongoConverter, который не может быть найден.
Сатья Нараян C

1

Я использую следующие экранирования в JavaScript для каждого ключа объекта:

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

Что мне нравится в нем, так это то, что он заменяет только $в начале и не использует символы Unicode, которые может быть сложно использовать в консоли. _для меня гораздо более читабелен, чем символ Юникода. Он также не заменяет один набор специальных символов ( $, .) другим (unicode). Но как следует ускользает от традиционного \.


3
И если кто-то использует _ в любом из своих ключей, вы получите ошибки.
БТ

1

Не идеально, но сработает в большинстве ситуаций: замените запрещенные символы чем-то другим. Поскольку он находится в ключах, эти новые символы должны быть довольно редкими.

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

Вот тест:

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

и результаты - обратите внимание, что значения не изменяются:

{
  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

Существует некрасивый способ запроса, не рекомендуется использовать его в приложении, а не для целей отладки (работает только со встроенными объектами):

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

Как упомянул другой пользователь, кодирование / декодирование может стать проблематичным в будущем, поэтому, вероятно, будет проще заменить все ключи с точкой. Вот рекурсивная функция, которую я сделал для замены клавиш на '.' случаи:

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)

Вы также можете изменить этот код, заменив '$', так как это еще один символ, который mongo не допускает в ключе.


0

Для PHP я заменяю значение HTML на точку. Вот это ".".

Он хранится в MongoDB следующим образом:

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

и код PHP ...

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


0

Вы можете сохранить его как есть и преобразовать в красивый после

Я написал этот пример на Livescript. Вы можете использовать веб-сайт livescript.net для его оценки

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

Это произведет

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


0

Дам вам мой совет: вы можете использовать JSON.stringify для сохранения объекта / массива, содержащего имя ключа, имеющего точки, затем проанализируйте строку для объекта с помощью JSON.parse для обработки при получении данных из базы данных

Другой обходной путь: измените структуру схемы, например:

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

0

Последняя версия MongoDB поддерживает ключи с точкой, но java-драйвер MongoDB не поддерживает. Чтобы заставить его работать на Java, я вытащил код из репозитория github java-mongo-driver и внес соответствующие изменения в их функцию isValid Key, создал из него новую банку, используя ее сейчас.


0

Замените точку ( .) или доллар ( $) другими символами, которые никогда не будут использоваться в реальном документе. И восстановите точку ( .) или доллар ( $) при извлечении документа. Стратегия не повлияет на данные, которые читает пользователь.

Вы можете выбрать персонажа из всех персонажей .


0

Как ни странно, используя mongojs, я могу создать документ с точкой, если сам установил _id, однако я не могу создать документ, когда создается _id:

Работает:

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

Не работает:

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

Сначала я подумал, что обновление документа с помощью ключа с точкой тоже работает, но это определение точки как подключ!

Увидев, как mongojs обрабатывает точку (подключ), я собираюсь убедиться, что мои ключи не содержат точки.


0

Как и то, что упомянул @JohnnyHK , удалите знаки препинания или "." из ваших ключей, потому что это создаст гораздо более серьезные проблемы, когда ваши данные начнут накапливаться в больший набор данных. Это вызовет проблемы, особенно когда вы вызываете агрегатные операторы, такие как $ merge, который требует доступа и сравнения ключей, что вызывает ошибку. Я усвоил это на собственном горьком опыте, пожалуйста, не повторяйте для тех, кто только начинает.


-2

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

Нашел в сообщениях об ошибках. Если вы используете anaconda(найдите соответствующий файл, если нет), просто измените значение с check_keys = Trueна Falseв файле, указанном выше. Это сработает!

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.