Как решить проблему с часовым поясом при хранении дат в utc с помощью mongod?

У меня есть коллекция mongodb, в которой каждый документ имеет некоторые атрибуты и временную метку utc. Мне нужно извлечь данные из коллекции и использовать структуру агрегации, потому что я использую данные из коллекции для отображения некоторых диаграмм в пользовательском интерфейсе. Однако мне нужно выполнить агрегацию в соответствии с часовым поясом пользователя. Предполагая, что я знаю часовой пояс пользователя (переданный в запросе из браузера или каким-либо другим образом), есть ли способ использовать структуру агрегации для агрегирования на основе часового пояса [клиента]?


person Hrishi    schedule 17.08.2013    source источник
comment
Можете ли вы прояснить, что вы пытаетесь сделать, почему вы не можете просто преобразовать в агрегацию сообщений часового пояса клиента? Вы всегда можете использовать mapreduce вместо фреймворка агрегации — это будет медленнее, но позволит выполнять любые специальные вычисления, которые вам нужны.   -  person Mason    schedule 17.08.2013
comment
На самом деле мне нужно генерировать еженедельные отчеты для пользователя на основе его/ее часового пояса. Для отчета мне нужно использовать структуру агрегации. Проблема в том, что я должен учитывать часовой пояс пользователя при агрегировании. Я не могу сделать это после агрегации, так как это приведет к неверным результатам. Уменьшение карты — это вариант, но мне это нужно по требованию, и при поиске в StackOverflow я увидел, что его не следует использовать вместо запроса. Я надеялся, что будет какой-то способ.   -  person Hrishi    schedule 17.08.2013
comment
Я не знаю о MongoDB достаточно хорошо, чтобы ответить, но я считаю, что подход будет аналогичен тому, что я описываю для RavenDB здесь и снова в Foo_ByDate_MultiZone индексируйте здесь. Если бы Монго допускал что-то подобное, вы бы сделали это на Карте, как я сделал в Raven.   -  person Matt Johnson-Pint    schedule 17.08.2013
comment
К сожалению, с инфраструктурой агрегации вы ограничены предоставленными операторами (именно поэтому это на порядок быстрее, чем mapreduce). MongoDB не предоставляет никаких функций часового пояса, он ожидает, что это будет сделано на уровне приложения. Можете ли вы объяснить, какие вычисления вы пытаетесь выполнить и почему их нужно преобразовать в часовой пояс в агрегации, а не в коде вашего приложения? Возможно, вы сможете сохранить смещение, представляющее смещение пользователя от UTC, и использовать это   -  person Mason    schedule 17.08.2013
comment
@Mason,@Matt Johnson, спасибо за попытку помочь. Похоже, Mongodb не предоставляет функции часового пояса. Да, сохранение смещения кажется перспективным. Я хотел, чтобы произошло преобразование часового пояса, потому что я хотел показать пользователю график его/ее активности за неделю в моем приложении. А поскольку они могут находиться в разных часовых поясах, диаграмма должна показывать им данные в соответствии с их часовым поясом.   -  person Hrishi    schedule 17.08.2013
comment
Нет причин, по которым вам нужно делать это в MongoDB. Вот для чего нужен прикладной уровень. Сделайте всю свою агрегацию в UTC, а затем на выходе отрегулируйте их часовой пояс. Хранение всего в формате UTC гарантирует, что все останется согласованным, даже если пользователь изменит часовой пояс.   -  person Mason    schedule 17.08.2013
comment
Если вы используете драйвер python, у него есть возможность выполнить преобразование за вас на стороне драйвера; вам придется сделать преобразование самостоятельно с другими драйверами. api.mongodb.org/python/current/api/pymongo/connection. html   -  person Dylan Tong    schedule 18.08.2013
comment
@Mason - В подавляющем большинстве случаев я согласен с вами, что код приложения - лучшее место для этого. Разница здесь в том, что он находится в агрегации. Чтобы сгруппировать по дням, нужно знать границы дня. Каждый часовой пояс имеет свою концепцию начала и конца дня, как в необработанном выражении UTC, так и в отношении изменений для летнего времени. Если Mongo не позволяет вам сделать это в индексной карте, тогда другим подходом будет предварительное вычисление нескольких местных времен в разных зонах перед выполнением агрегирования и сохранение их вместе с документом. Это становится грязным, хотя.   -  person Matt Johnson-Pint    schedule 18.08.2013


Ответы (3)


То, о чем вы просите, в настоящее время обсуждается в выпуске MongoDB SERVER-6310.

Я нашел это по ссылке из ветки обсуждения.

Проблема характерна для любой группировки по дате, включая базы данных SQL и базы данных NoSQL. Фактически, я недавно обратился к этому в RavenDB. Существует хорошее описание проблемы и решение RavenDB здесь.

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

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

ОБНОВЛЕНИЕ: эта проблема была решена в MongoDB в июле 2017 года (версия 3.5.11). Решение описано в первой ссылке выше, но вкратце они ввели новый формат объектов для дат в выражениях агрегации. : { date: <dateExpression>, timezone: <tzExpression> }, который позволяет указать часовой пояс для использования при агрегировании. См. здесь другой пример в документации Mongo.

person Matt Johnson-Pint    schedule 18.08.2013
comment
Спасибо. Надеюсь, MongoDB предоставит такую ​​возможность в будущем. - person Hrishi; 18.08.2013
comment
@Matt, есть ли какое-либо решение для этого в последней версии mongodb 2.6.x? - person Rams; 18.07.2014
comment
@rams - я не проверял, но проблема по-прежнему помечена как открытая/нерешенная. - person Matt Johnson-Pint; 18.07.2014

Помимо SERVER-6310, упомянутого Мэттом Джонсоном, еще одним обходным путем является использование оператора $project для добавления или вычитания из часового пояса UTC, чтобы «сдвинуть время» в правильную локальную зону. Оказывается, вы можете добавить или вычесть время в миллисекундах.

Например, предположим, что у меня есть поле даты с именем orderTime. Я хотел бы запросить EDT. Это -4 часа от UTC. Это 4 * 60 * 60 * 1000 миллисекунд.

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

db.table.aggregate( 
    { $project : { orderTimeLocal : { $subtract : [ "$orderTime", 14400000] } } },
    { $project : { day_ordered : { $dayOfYear : "$orderTimeLocal" } } })
person Astral    schedule 20.08.2013
comment
Спасибо Астрал. Ваш ответ идеален. Я действительно не хотел использовать уменьшение карты только для простой настройки часового пояса. - person cfchris; 22.11.2013
comment
Этот ответ имеет большой смысл. Если вы хотите запросить восточное время, которое включает в себя как EST, так и EDT, как бы вы это сделали? - person Joe; 17.04.2014
comment
Разве это не "$lastActivity" в объяснении, а не "$orderTime" в ответе? - person digitalextremist; 24.01.2015
comment
Это не сработает из-за перехода на летнее время. Смещение может быть -4 или -5 в зависимости от дня/месяца. - person Ricardo Macario; 07.04.2015
comment
Согласен с Рикардо Макарио - person Jin Thakur; 01.12.2016
comment
Эта реализация БУДЕТ работать с летним временем, если вы используете библиотеку часовых поясов, которая может получить текущее смещение UTC для текущего дня. Итак: 4 * 60 * 60 * 1000 превращается в utcOffset * 60 * 60 * 1000 - person daino3; 14.07.2017

Все предложенные выше подходы прекрасно работают, но поскольку существует новая версия mongodb, начиная с 2.6, вы можете использовать $let в структуре агрегации, это позволит вам создавать переменные на лету, избегая, таким образом, необходимости $project перед группировкой. Теперь вы можете создать переменную с $let, которая будет содержать локализованное время, и использовать ее в операторе $group.

Что-то вроде:

db.test.aggregate([
   {$group: { 
        _id: { 
             $let: { 
                 vars: {  
                     local_time: { $subtract: ["$date", 10800000]} 
                 }, 
                 in: { 
                    $concat: [{$substr: [{$year: "$$local_time"}, 0, 4]}, 
                              "-", 
                              {$substr: [{$month: "$$local_time"}, 0, 2]}, 
                              "-", 
                              {$substr: [{$dayOfMonth: "$$local_time"}, 0, 2]}]
                 }
              }
         }, 
         count: {$sum: 1}
     }
 }])

Обратите внимание, что вы используете $let внутри определения блока/переменной, и значение этого блока/переменной является возвращаемым значением подвыражения "in", где используются определенные выше переменные.

person Sebastian    schedule 23.04.2014
comment
Это также не работает в случаях, связанных с переходом на летнее время, поскольку вы предполагаете, что фиксированное время может быть вычтено, тогда как правильное значение может отличаться на час в зависимости от того, находится ли местное время в режиме летнего времени или нет. - person Mark Stosberg; 03.04.2017