强制 Google Cloud 的数据存储日期时间默认为 UTC

Forcing Google Cloud's Datastore datetimes to default to UTC

根据数据存储 docs,日期时间始终以 UTC 格式存储和返回。

但是,在云仪表板中,我的所有日​​期时间都显示在 CEST 中,即 UTC+2

保存实体时,我不包括任何时区并使用 datetime.utcnow()

更具体地说,这是我的 TimeZonedDateTimeProperty 模型,extending/partially 覆盖 google 自己的 db.DateTimeProperty:

class TimeZonedDateTimeProperty(db.DateTimeProperty):
    def __init__(self, *args, **kwargs):
        super(TimeZonedDateTimeProperty, self).__init__(*args, **kwargs)

    def __get__(self, model_instance, model_class):
        """Returns the value for this property on the given model instance."""

        if model_instance is None:
            return self

        attr_to_return = None

        try:
            # access the db.DateTimeProperty's getter

            attr_to_return = getattr(model_instance, super(TimeZonedDateTimeProperty, self)._attr_name())
        except AttributeError:
            attr_to_return = None

        if attr_to_return:
            # super methods of db.DateTimeProperty will be calling this 
            # getter too and we don't wanna mess up
            # the UTC convention when working with datetimes

            includes_correct_caller = None

            try:
                # after noticing that this getter is also being called
                # outside of user requests, inspect the caller and decide
                # whether to offset the UTC datetime or not 

                includes_correct_caller = "main_class_name" in str(inspect.stack()[1][0].f_locals["self"].__class__)
            except Exception:
                includes_correct_caller = False

            if includes_correct_caller:
                return attr_to_return + relativedelta(hours=+2)
            else:
                return attr_to_return
        else:
            return None

下面是子类 db.Model 中的示例用法:

class Message(db.Model):
    ...
    sent = TimeZonedDateTimeProperty()
    ...


m = Message(...
            sent=datetime.utcnow(),
            ...)
m.put()

现在,如果我在本地主机上调用 m.sent 运行,事情会按计划进行,即 sent 属性 以 UTC 保存,getter returns 偏移日期时间。

在 live/production 环境中,属性 以 UTC 格式保存,因此,被覆盖的 getter 不必要地增加了 2 小时。

所以:

谢谢!

固定

基于 getter 确实在标准 get 请求之外调用的事实,尤其是在执行 put() 时,只需观察调用堆栈并查找put 关键字。

换句话说,在将 UTC 时间戳正确保存到数据库之前阻止修改。

...

includes_correct_caller = None

try:
    _, _, _, stack_trace, _, _ = zip(*list(inspect.stack()))
    includes_correct_caller = "put" not in stack_trace    
except Exception:
    includes_correct_caller = False

if includes_correct_caller:
    return attr_to_return + relativedelta(hours=+2)
else:
    return attr_to_return

Google 数据存储 以 UTC(0) 格式存储所有 dates/times。但是,云仪表板与您机器的时区有关,因此日期是根据您的操作系统设置的时区呈现的。如果您使用的是基于 Window 的系统,请尝试更改时区,然后刷新仪表板。您会发现所列实体的任何日期属性都会更新以反映您系统的新时区。

为了回答第一个问题,App Engine 已经将时间存储为 UTC。

针对第二个问题,问题中的方法和OP的回答会在未来造成问题:

  • 当中欧夏令时 (CEST) 结束和中欧时间 (CET) 开始时,它会产生不正确的值
  • 检查堆栈以避免复合偏移量可能不起作用,例如,如果 Message.sent 是在另一个对象的 put 方法中检索到的
  • 如果应用程序增长到使用更多时区,这将导致复杂化

可以通过将日期时间存储在标准 DateTimeProperty 中并在需要时将它们转换为时区感知日期时间来避免这些问题。

db docs 为 OP 的用例提出了一个解决方案:

If you only convert values to and from a particular time zone, you can implement a custom datetime.tzinfo to convert values from the datastore

实施自定义 tzinfo 可能很棘手,但是 dateutil 包中已经实施了 tzinfos。

该模型可以有一个方法 returns 时区感知日期时间:

from dateutil import tz
from google.appengine.ext import db

class Message(db.Model):

    sent = db.DateTimeProperty()

    def local_sent(self, default_tz='Europe/Paris'):
        """
        Returns a timezone-aware datetime for the local
        timezone.  
        The default timezone can be overridden to support
        other locales. 
        """
        utc = tz.gettz('UTC')
        local = tz.gettz(tz)    # or some other CET timezone
        return self.sent.replace(tzinfo=utc).astimezone(local)

用法:

m = Message(sent=datetime.datetime.utcnow())
m.sent
datetime.datetime(2017, 6, 16, 18, 0, 12, 364017)
m.local_sent()
datetime.datetime(2017, 6, 16, 20, 0, 12, 364017, tzinfo=tzfile('/usr/share/zoneinfo/Europe/Paris'))
m.local_sent.strftime('%Y-%m-%d %H:%M:%S.%f %z %Z')
'2017-06-16 20:00:12.364017 +0200 CEST'

m = Message(sent=datetime.datetime.utcnow())
2017-06-16 18:15:33.555208
2017-06-16 18:15:33.555208  
m.local_sent('Asia/Beijing')
datetime.datetime(2017, 6, 17, 2, 15, 33, 555208, tzinfo=tzfile('/usr/share/zoneinfo/Asia/Beijing'))
m.local_sent('Asia/Beijing').strftime('%Y-%m-%d %H:%M:%S.%f %z %Z')
'2017-06-17 02:15:33.555208 +0800 CST'

这种方法提供了正确的夏令时行为,避免了可能脆弱的变通办法并处理了大多数时区。