SQLAlchemy 日期时间时区

问题描述 投票:0回答:5

SQLAlchemy 的

DateTime
类型允许使用
timezone=True
参数将非幼稚日期时间对象保存到数据库,并按原样返回它。有没有办法修改 SQLAlchemy 传入的
tzinfo
的时区,例如 UTC?我意识到我可以只使用
default=datetime.datetime.utcnow
;然而,这是一个天真的时间,它会很乐意接受有人传入一个天真的基于本地时间的日期时间,即使我使用了
timezone=True
,因为它使本地或 UTC 时间变得不天真,而没有基本时区来规范化它。我尝试过(使用 pytz)使日期时间对象变得非天真,但是当我将其保存到数据库时,它又返回为天真的。

注意 datetime.datetime.utcnow 与

timezone=True
的配合效果不佳:

import sqlalchemy as sa
from sqlalchemy.sql import select
import datetime

metadata = sa.MetaData('postgres://user:pass@machine/db')

data_table = sa.Table('data', metadata,
    sa.Column('id',   sa.types.Integer, primary_key=True),
    sa.Column('date', sa.types.DateTime(timezone=True), default=datetime.datetime.utcnow)
)

metadata.create_all()

engine = metadata.bind
conn = engine.connect()
result = conn.execute(data_table.insert().values(id=1))

s = select([data_table])
result = conn.execute(s)
row = result.fetchone()

(1, 日期时间.日期时间(2009, 1, 6, 0, 9, 36, 891887))

row[1].utcoffset()

datetime.timedelta(-1, 64800) # 这是我的本地时间偏移!!

datetime.datetime.now(tz=pytz.timezone("US/Central"))

日期时间.timedelta(-1, 64800)

datetime.datetime.now(tz=pytz.timezone("UTC"))

datetime.timedelta(0) #UTC

即使我将其更改为明确使用 UTC:

...

data_table = sa.Table('data', metadata,
    sa.Column('id',   sa.types.Integer, primary_key=True),
    sa.Column('date', sa.types.DateTime(timezone=True), default=datetime.datetime.now(tz=pytz.timezone('UTC')))
)

row[1].utcoffset()

...

datetime.timedelta(-1, 64800) # 它没有使用我明确添加的时区

或者如果我放弃

timezone=True
:

...

data_table = sa.Table('data', metadata,
    sa.Column('id',   sa.types.Integer, primary_key=True),
    sa.Column('date', sa.types.DateTime(), default=datetime.datetime.now(tz=pytz.timezone('UTC')))
)

row[1].utcoffset() is None

...

True # 这次甚至没有将时区保存到数据库中

python postgresql datetime sqlalchemy timezone
5个回答
36
投票

http://www.postgresql.org/docs/8.3/interactive/datatype-datetime.html#DATATYPE-TIMEZONES

所有时区感知的日期和时间均以 UTC 内部存储。在显示给客户端之前,它们会转换为时区配置参数指定的区域中的本地时间。

用postgresql存储它的唯一方法就是单独存储。


18
投票

解决此问题的一种方法是始终使用数据库中的时区感知字段。但请注意,根据时区的不同,相同的时间可以有不同的表达方式,尽管这对计算机来说不是问题,但对我们来说却很不方便:

2003-04-12 23:05:06 +01:00
2003-04-13 00:05:06 +02:00 # This is the same time as above!

Postgresql 还以 UTC 形式在内部存储所有时区感知的日期和时间。在显示给客户端之前,它们会转换为时区配置参数指定的区域中的本地时间。

相反,我建议在整个应用程序和数据库中的时区原始日期和时间中使用

UTC
时间戳,并且仅在用户看到它们之前将它们转换为用户本地时区。

此策略可让您拥有最干净的代码,避免任何时区转换和混乱,并使您的数据库和应用程序始终独立于“本地时区”差异而工作。例如,您的开发机器和生产服务器可能在不同时区的云上运行。

要实现此目的,请在初始化引擎之前告诉 Postgresql 您希望查看 UTC 时区。

在 SqlAlchemy 中你可以这样做:

engine = create_engine(..., connect_args={"options": "-c timezone=utc"})

如果您正在使用tornado-sqlalchemy,您可以使用:

factory = make_session_factory(..., connect_args={"options": "-c timezone=utc"})

由于我们在任何地方都使用所有 UTC 时区,因此我们只需在模型中使用时区朴素的日期和时间:

created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime)

如果您使用 alembic,则同样:

sa.Column('created_at', sa.DateTime()),
sa.Column('updated_at', sa.DateTime()),

并在代码中使用UTC时间:

from datetime import datetime
...
model_object.updated_at = datetime.now(timezone.utc)

11
投票

解决方案在这个问题的答案中给出:

您可以通过以 UTC 格式存储数据库中的所有(日期)时间对象,并在检索时将生成的原始日期时间对象转换为感知对象来规避此问题。

唯一的缺点是你会丢失时区信息,但无论如何,将日期时间对象存储在 utc 中可能是个好主意。

如果您关心时区信息,我会单独存储它,并且仅在最后可能的情况下(例如在显示之前)将 utc 转换为本地时间

或者也许您根本不需要关心,并且可以使用运行程序的计算机上的本地时区信息,或者用户的浏览器(如果它是网络应用程序)。


5
投票

建议使用以下结构在数据库中存储 UTC 日期和时间数据,并防止存储没有此类位置信息的数据。

import datetime
from sqlalchemy import DateTime
from sqlalchemy.types import TypeDecorator

    
class TZDateTime(TypeDecorator):
    """
    A DateTime type which can only store tz-aware DateTimes.
    """
    impl = DateTime(timezone=True)

    def process_bind_param(self, value, dialect):
        if isinstance(value, datetime.datetime) and value.tzinfo is None:
            raise ValueError('{!r} must be TZ-aware'.format(value))
        return value

    def __repr__(self):
        return 'TZDateTime()'

数据库中存储的值应定义如下:

import datetime

import pytz


def tzware_datetime():
    """
    Return a timezone aware datetime.

    :return: Datetime
    """
    return datetime.datetime.now(pytz.utc)

0
投票

可以传递日期时间字符串作为另一种解决方案。

from datetime import datetime
from datetime import timedelta
from datetime import timezone
from pprint import pprint
from typing import Literal


TTimezoneFmt = Literal['', '%z', '%Z', '%:z']


def get_datetime_now(offset: int, tz_fmt: TTimezoneFmt = '') -> str:
    tz = timezone(offset=timedelta(hours=offset))
    dt_now = datetime.now(tz=tz)
    dt_fmt = f'%Y-%m-%dT%H:%M:%S.%f {tz_fmt}'.strip()
    return dt_now.strftime(dt_fmt)


times = {
    'utc': {
        'utc1': get_datetime_now(0),
        'utc2': get_datetime_now(0, tz_fmt='%:z'),
        'utc3': get_datetime_now(0, tz_fmt='%Z'),
        'utc4': get_datetime_now(0, tz_fmt='%z'),
    },
    'msc': {
        'msc1': get_datetime_now(3),
        'msc2': get_datetime_now(3, tz_fmt='%:z'),
        'msc3': get_datetime_now(3, tz_fmt='%Z'),
        'msc4': get_datetime_now(3, tz_fmt='%z'),
    },
}

pprint(times, sort_dicts=False)
{'utc': {'utc1': '2024-04-19T14:16:45.614518',
         'utc2': '2024-04-19T14:16:45.614559 +00:00',
         'utc3': '2024-04-19T14:16:45.614577 UTC',
         'utc4': '2024-04-19T14:16:45.614592 +0000'},
 'msc': {'msc1': '2024-04-19T17:16:45.614607',
         'msc2': '2024-04-19T17:16:45.614619 +03:00',
         'msc3': '2024-04-19T17:16:45.614632 UTC+03:00',
         'msc4': '2024-04-19T17:16:45.614647 +0300'}}
© www.soinside.com 2019 - 2024. All rights reserved.