В своем проекте на 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-битной платформе):
Показывало:
В двоичной форме:
Обрезка данных — очевидно не очень хорошая идея. Чтобы решить эту проблему, мы могли бы просто позволить стандартному типу integer PHP быть переданным напрямую в MongoDB. Но вместо изменения того, как драйвер MongoDB работает по умолчанию, я добавил новую настройку mongo.native_long — просто потому, что иначе мы могли бы сломать некоторые работающие приложения. С включенной настройкой mongo.native_long, мы видим другой результат выполнения скрипта:
Этот скрипт покажет:
На 64-битных платформах, настройка mongo.native_long позволяет сохранять 64-битные целые в MongoDB. Тип данных MongoDB, который используется в данном случае — BSON LONG, вместо BSON INT, который используется, если эту настройку выключить. Настройка также меняет поведение BSON LONG данных при чтении обратно из MongoDB. Без включенной настройки mongo.native_long, драйвер преобразовал бы все BSON LONG в PHP тип float, что привело бы к потере точности. Вы можете увидеть это на следующем примере:
Этот скрипт покажет:
На 32-битных платформах настройка mongo.native_long ничего не меняет при сохранении целых чисел в MongoDB: число будет сохранено в виде BSON INT, как и раньше. Однако, при чтении BSON LONG чисел из MongoDB с включенной настройкой на 32-битной платформе будет выброшено исключение MongoCursorException, предупреждающее вас о том, что данные не могут быть прочитаны без потери точности:
Если настройка выключена, BSON LONG будет преобразован в PHP тип float, чтобы не терять обратной совместимости с предыдущим поведением драйвера.
Несмотря на то, что настройка mongo.native_long позволяет использовать 64-битные числа на 64-битных платформах, она ничего не дает на 32-битных платформах, кроме защиты от потери данных при чтении BSON LONG значений — и то только путем выбрасывания исключения.
Как часть работы по обеспечению надежной работы с 64-битными числами в MongoDB из PHP, я также добавил два новых класса: MongoInt32 и MongoInt64. Эти два класса — простые обертки вокруг строкового представления числа. Они создаются так:
Вы можете использовать эти объекты в обычных запросах на вставку и модификацию данных, как нормальные числа:
Вывод:
Как видно из примера, ничего не изменилось в чтении значений из базы. 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. Следующий скрипт показывает это:
Вывод скрипта:
Классы MongoInt32 и MongoInt64 реализуют метод __toString(), чтобы их значения могли быть выведены через echo. Вы можете получить их значения только как строки. Пожалуйста, обратите внимание, что MongoDB чувствителен к типам, и не воспримет число, содержащееся в строке, как число. Этот скрипт показывает это (на 64-битной платформе):
Вывод:
Следующие таблицы показывают, как работают все различные конвертации чисел в зависимости от включенных настроек:
PHP -> MongoDB на 32-битных платформах
PHP -> MongoDB на 64-битных платформах
MongoDB -> PHP на 32-битных платформах
MongoDB -> PHP на 64-битных платформах
Conclusion
Как мы заметили, получение поддержки 64-битных целых на PHP с MongoDB может быть нетривиальным делом. Мои рекомендации — использовать mongo.native_long=1, если вы работаете только с 64-битными платформами в своем коде. В этом случае, все целые числа, которые вы запишете в базу, вернутся оттуда также как целые числа в исходном виде, даже если они — 64-битные.
Если же вам приходится работать с 32-битными платформами (сюда входят и 64-битные билды PHP для Windows!), то вы не можете для хранения 64-битных чисел использовать просто стандартный тип integer в PHP, вам придется использовать класс MongoInt64, а значит и работать со строковыми представлениями чисел. Вам также нужно иметь ввиду, что консоль MongoDB считает все числа числами с плавающей точкой (float), и что она не может отобразить 64-битные целые числа. Вместо этого она покажет их как float. Не пытайтесь модифицировать эти числа в консоли, это изменит их тип.
Например, после выполнения скрипта:
консоль MongoDB (mongo) будет вести себя так:
Разумеется, при чтении данных через драйвер, поддерживающий 64-битные целые, вы получите правильный результат:
покажет:
Новая функциональность, описанная в этой статье — часть релиза mongo 1.0.9, который доступен через PECL с помощью команды pecl install mongo.
Удачи с вашими 64-битными целыми числами!
P.S. Это мой первый перевод, просьба сильно ногами не пинать :)
Текущий проект, над которым я работаю, основан на 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. Это мой первый перевод, просьба сильно ногами не пинать :)