Pull to refresh

64-битные целые в MongoDB

Reading time 7 min
Views 6.6K
Original author: Derick Rethans
В своем проекте на PHP пришлось столкнуться с необходимостью хранения в базе 64-битных целых данных. Нашел только одну статью по теме, зато очень подробную (местами даже слишком) и объясняющую все тонкости. Решил опубликовать перевод на Хабре, на случай, если кто-нибудь столкнется с аналогичной проблемой.



Текущий проект, над которым я работаю, основан на MongoDB, мосте между хранилищами типа «ключ-значение» и традиционными РСУБД. Пользователи в этом проекте идентифицируются по их Facebook UserID, который является 64-битным целым числом. К сожалению, Драйвер MongoDB для PHP имел поддержку только для 32-битных целых чисел, что вызывало проблемы с новыми пользователями Facebook. Новый классный длинный UserID у них обрезался до 32 бит, из-за чего приложение работало некорректно.

Для внутреннего хранения документов MongoDB использует нечто, называемое BSON (Binary JSON). В BSON есть два целых числовых типа: 32-битное знаковое целое, называющееся INT и 64-битное знаковое целое, называющееся LONG. В документации к драйверу MongoDB для PHP сказано (или было сказано, в зависимости от того, когда вы это читаете), что поддерживаются только 32-битные целые типы, т.к. «PHP не поддерживает 8-байтовые целые». Это не совсем так. Тип integer в PHP поддерживает 64-битные значения на платформах, где тип long в C — 64-битный. Это любая 64-битная платформа (если PHP скомпилирован для 64-битной архитектуры), кроме Windows, где тип long в C всегда 32-битный.

Каждый раз, когда целое число передавалось из PHP в MongoDB, драйвер использовал только 32 младших значащих разряда для сохранения числа в документе. Пример ниже показывает, что происходило (на 64-битной платформе):

<?php
$m = new Mongo();
$c = $m->selectCollection('test', 'inttest');
$c->remove(array());

$c->insert(array('number' => 1234567890123456));

$r = $c->findOne();
echo $r['number'], "\n";
?>


Показывало:

int(1015724736)


В двоичной форме:

1234567890123456 = 100011000101101010100111100100010101011101011000000
      1015724736 =                      111100100010101011101011000000


Обрезка данных — очевидно не очень хорошая идея. Чтобы решить эту проблему, мы могли бы просто позволить стандартному типу integer PHP быть переданным напрямую в MongoDB. Но вместо изменения того, как драйвер MongoDB работает по умолчанию, я добавил новую настройку mongo.native_long — просто потому, что иначе мы могли бы сломать некоторые работающие приложения. С включенной настройкой mongo.native_long, мы видим другой результат выполнения скрипта:

<?php
ini_set('mongo.native_long', 1);
$c->insert(array('number' => 1234567890123456));

$r = $c->findOne();
var_dump($r['number']);
?>


Этот скрипт покажет:

int(1234567890123456)


На 64-битных платформах, настройка mongo.native_long позволяет сохранять 64-битные целые в MongoDB. Тип данных MongoDB, который используется в данном случае — BSON LONG, вместо BSON INT, который используется, если эту настройку выключить. Настройка также меняет поведение BSON LONG данных при чтении обратно из MongoDB. Без включенной настройки mongo.native_long, драйвер преобразовал бы все BSON LONG в PHP тип float, что привело бы к потере точности. Вы можете увидеть это на следующем примере:

<?php
ini_set('mongo.native_long', 1);
$c->insert(array('number' => 12345678901234567));

ini_set('mongo.native_long', 0);
$r = $c->findOne();
var_dump($r['number']);
?>


Этот скрипт покажет:

float(1.2345678901235E+16)


На 32-битных платформах настройка mongo.native_long ничего не меняет при сохранении целых чисел в MongoDB: число будет сохранено в виде BSON INT, как и раньше. Однако, при чтении BSON LONG чисел из MongoDB с включенной настройкой на 32-битной платформе будет выброшено исключение MongoCursorException, предупреждающее вас о том, что данные не могут быть прочитаны без потери точности:

MongoCursorException: Can not natively represent the long 1234567890123456 on this platform


Если настройка выключена, BSON LONG будет преобразован в PHP тип float, чтобы не терять обратной совместимости с предыдущим поведением драйвера.

Несмотря на то, что настройка mongo.native_long позволяет использовать 64-битные числа на 64-битных платформах, она ничего не дает на 32-битных платформах, кроме защиты от потери данных при чтении BSON LONG значений — и то только путем выбрасывания исключения.

Как часть работы по обеспечению надежной работы с 64-битными числами в MongoDB из PHP, я также добавил два новых класса: MongoInt32 и MongoInt64. Эти два класса — простые обертки вокруг строкового представления числа. Они создаются так:

<?php
$int32 = new MongoInt32("32091231");
$int64 = new MongoInt64("1234567980123456");
?>


Вы можете использовать эти объекты в обычных запросах на вставку и модификацию данных, как нормальные числа:

<?php
$m = new Mongo();
$c = $m->selectCollection('test', 'inttest');
$c->remove(array());

$c->insert(array(
        'int32' => new MongoInt32("1234567890"),
        'int64' => new MongoInt64("12345678901234567"),
));

$r = $c->findOne();
var_dump($r['int32']);
var_dump($r['int64']);
?>


Вывод:

int(1234567890)
float(1.2345678901235E+16)


Как видно из примера, ничего не изменилось в чтении значений из базы. BSON INT все также возвращается как целое, а BSON LONG — как float. Если мы включим настройку mongo.native_long, то BSON LONG, сохраненный с помощью класса MongoInt64 будет возвращен как целочисленный тип PHP на 64-битных платформах, а на 32-битных платформах мы получим MongoCursorException.

Чтобы получить 64-битные числа обратно из MongoDB на 32-битных платформах, я добавил еще одну настройку — mongo.long_as_object. Она (на любой платформе) включит возврат BSON LONG из MongoDB в виде объекта MongoInt64. Следующий скрипт показывает это:

<?php
$m = new Mongo();
$c = $m->selectCollection('test', 'inttest');
$c->remove(array());

$c->insert(array(
        'int64' => new MongoInt64("12345678901234567"),
));

ini_set('mongo.long_as_object', 1);
$r = $c->findOne();
var_dump($r['int64']);
echo $r['int64'], "\n";
echo $r['int64']->value, "\n";
?>


Вывод скрипта:

object(MongoInt64)#7 (1) {
  ["value"]=>
  string(17) "12345678901234567"
}
12345678901234567
12345678901234567


Классы MongoInt32 и MongoInt64 реализуют метод __toString(), чтобы их значения могли быть выведены через echo. Вы можете получить их значения только как строки. Пожалуйста, обратите внимание, что MongoDB чувствителен к типам, и не воспримет число, содержащееся в строке, как число. Этот скрипт показывает это (на 64-битной платформе):

<?php
ini_set('mongo.native_long', 1);

$m = new Mongo();
$c = $m->selectCollection('test', 'inttest');
$c->remove(array());

$nr = "12345678901234567";
$c->insert(array('int64' => new MongoInt64($nr)));

$r = $c->findOne(array('int64' => $nr)); // $nr is a string here
var_dump($r['int64']);
$r = $c->findOne(array('int64' => (int) $nr));
var_dump($r['int64']);
?>


Вывод:

NULL
int(12345678901234567)


Следующие таблицы показывают, как работают все различные конвертации чисел в зависимости от включенных настроек:

PHP -> MongoDB на 32-битных платформах

Исходное значение native_long=0 native_long=1
1234567 INT(1234567) INT(1234567)
123456789012 FLOAT(123456789012) FLOAT(123456789012)
MongoInt32(«1234567») INT(1234567) INT(1234567)
MongoInt64(«123456789012») LONG(123456789012) LONG(123456789012)


PHP -> MongoDB на 64-битных платформах

Исходное значение native_long=0 native_long=1
1234567 INT(1234567) LONG(1234567)
123456789012 garbage LONG(123456789012)
MongoInt32(«1234567») INT(1234567) INT(1234567)
MongoInt64(«123456789012») LONG(123456789012) LONG(123456789012)


MongoDB -> PHP на 32-битных платформах

В MongoDB long_as_object=0, native_long=0 long_as_object=0, native_long=1 long_as_object=1
INT(1234567) int(1234567) int(1234567) int(1234567)
LONG(123456789012) float(123456789012) MongoCursorException MongoInt64(«123456789012»)


MongoDB -> PHP на 64-битных платформах

В MongoDB long_as_object=0, native_long=0 long_as_object=0, native_long=1 long_as_object=1
INT(1234567) int(1234567) int(1234567) int(1234567)
LONG(123456789012) float(123456789012) int(123456789012) MongoInt64(«123456789012»)


Conclusion

Как мы заметили, получение поддержки 64-битных целых на PHP с MongoDB может быть нетривиальным делом. Мои рекомендации — использовать mongo.native_long=1, если вы работаете только с 64-битными платформами в своем коде. В этом случае, все целые числа, которые вы запишете в базу, вернутся оттуда также как целые числа в исходном виде, даже если они — 64-битные.

Если же вам приходится работать с 32-битными платформами (сюда входят и 64-битные билды PHP для Windows!), то вы не можете для хранения 64-битных чисел использовать просто стандартный тип integer в PHP, вам придется использовать класс MongoInt64, а значит и работать со строковыми представлениями чисел. Вам также нужно иметь ввиду, что консоль MongoDB считает все числа числами с плавающей точкой (float), и что она не может отобразить 64-битные целые числа. Вместо этого она покажет их как float. Не пытайтесь модифицировать эти числа в консоли, это изменит их тип.

Например, после выполнения скрипта:

<?php
$m = new Mongo();
$c = $m->selectCollection('test', 'inttest');
$c->remove(array());

$c->insert(array('int64' => new MongoInt64("123456789012345678")));


консоль MongoDB (mongo) будет вести себя так:

$ mongo
MongoDB shell version: 1.4.4
url: test
connecting to: test
type "help" for help
> use test
switched to db test
> db.inttest.find()
{ "_id" : ObjectId("4c5ea6d59a14ce1319000000"), "int64" : { "floatApprox" : 123456789012345680, "top" : 28744523, "bottom" : 2788225870 } }


Разумеется, при чтении данных через драйвер, поддерживающий 64-битные целые, вы получите правильный результат:

ini_set('mongo.long_as_object', 1);
$r = $c->findOne();
var_dump($r['int64']);
?>


покажет:

object(MongoInt64)#7 (1) {
  ["value"]=>
  string(18) "123456789012345678"
}


Новая функциональность, описанная в этой статье — часть релиза mongo 1.0.9, который доступен через PECL с помощью команды pecl install mongo.
Удачи с вашими 64-битными целыми числами!

P.S. Это мой первый перевод, просьба сильно ногами не пинать :)
Tags:
Hubs:
+37
Comments 24
Comments Comments 24

Articles