SQLAlchemy + SQLite 的 "create_function" 日期时间表示问题
我们有一些sqlite数据库,里面的日期时间实际上是以Excel格式存储的(这样做是有原因的;这是我们系统选择的标准表示方式,而且这些sqlite数据库可能会被多种语言和系统访问)。
最近几个月,我们成功地将Python引入到这个系统中,SQLAlchemy也是其中的一部分。sqlite3的dbapi层能够快速绑定自定义的Python函数,这一点特别受欢迎,因为SQLite本身缺少某些SQL函数。
我写了一个ExcelDateTime类型的装饰器,这在从sqlite数据库中获取结果集时工作得很好;Python能够正确地返回日期时间。
不过,我在绑定那些期望输入参数为Python日期时间的自定义函数时遇到了麻烦;我本以为这就是bindparam的用途,但显然我错过了什么,因为我无法让这个场景正常工作。不幸的是,修改这些函数以将Excel日期时间转换为Python日期时间并不是一个选项,而且改变数据库中日期时间的表示方式也不行,因为可能有多个系统或语言会访问这些数据。
下面的代码是一个可以直接运行的完整示例,代表了这个问题。自定义函数“get_month”被创建,但失败了,因为它接收到的是原始数据,而不是“Born”列中经过类型转换的数据。最后你可以看到我到目前为止尝试过的内容,以及它抛出的错误……
我想做的事情是不可行的吗?还是说有其他方法可以确保绑定的函数接收到合适的Python类型?这是我目前无法克服的唯一问题,找到解决方案会非常棒!
import sqlalchemy.types as types
from sqlalchemy import create_engine, Table, Column, Integer, String, MetaData
from sqlalchemy.sql.expression import bindparam
from sqlalchemy.sql import select, text
from sqlalchemy.interfaces import PoolListener
import datetime
# setup type decorator for excel<->python date conversions
class ExcelDateTime( types.TypeDecorator ):
impl = types.FLOAT
def process_result_value( self, value, dialect ):
lxdays = int( value )
lxsecs = int( round((value-lxdays) * 86400.0) )
if lxsecs == 86400:
lxsecs = 0
lxdays += 1
return ( datetime.datetime.fromordinal(lxdays+693594)
+ datetime.timedelta(seconds=lxsecs) )
def process_bind_param( self, value, dialect ):
if( value < 200000 ): # already excel float?
return value
elif( isinstance(value,datetime.date) ):
return value.toordinal() - 693594.0
elif( isinstance(value,datetime.datetime) ):
date_part = value.toordinal() - 693594.0
time_part = ((value.hour*3600) + (value.minute*60) + value.second) / 86400.0
return date_part + time_part # time part = day fraction
# create sqlite memory db via sqlalchemy
def get_month( dt ):
return dt.month
class ConnectionFactory( PoolListener ):
def connect( self, dbapi_con, con_record ):
dbapi_con.create_function( 'GET_MONTH',1,get_month )
eng = create_engine('sqlite:///:memory:',listeners=[ConnectionFactory()])
eng.dialect.dbapi.enable_callback_tracebacks( 1 ) # show better errors from user functions
meta = MetaData()
birthdays = Table('Birthdays', meta, Column('Name',String,primary_key=True), Column('Born',ExcelDateTime), Column('BirthMonth',Integer))
meta.create_all(eng)
dbconn = eng.connect()
dbconn.execute( "INSERT INTO Birthdays VALUES('Jimi Hendrix',15672,NULL)" )
# demonstrate the type decorator works and we get proper datetimes out
res = dbconn.execute( select([birthdays]) )
tuple(res)
# >>> ((u'Jimi Hendrix', datetime.datetime(1942, 11, 27, 0, 0)),)
# simple attempt (blows up with "AttributeError: 'float' object has no attribute 'month'")
dbconn.execute( text("UPDATE Birthdays SET BirthMonth = GET_MONTH(Born)") )
# more involved attempt( blows up with "InterfaceError: (InterfaceError) Error binding parameter 0 - probably unsupported type")
dbconn.execute( text( "UPDATE Birthdays SET BirthMonth = GET_MONTH(:Born)",
bindparams=[bindparam('Born',ExcelDateTime)],
typemap={'Born':ExcelDateTime} ),
Born=birthdays.c.Born )
非常感谢。
1 个回答
与其让Excel或微软来决定你如何存储日期和时间,不如按照标准的“显而易见的方式”来做,这样会更省事。
根据各自领域的标准来处理对象——在Python/SQLAlchemy中使用Python的方式(datetime对象),在SQLite中使用SQL的方式(使用原生的日期/时间类型,而不是浮点数!)。
使用API在不同领域之间进行必要的转换。(Python通过SQLAlchemy与SQLite沟通,通过xlrd/xlwt与Excel沟通,Python还可以与其他系统沟通,Python就像是你的粘合剂。)
在SQLite中使用标准的日期/时间类型,可以让你用标准的可读方式写SQL,而不需要Python参与(比如WHERE date BETWEEN '2011-11-01' AND '2011-11-02'
比WHERE date BETWEEN 48560.9999 AND 48561.00001
更容易理解)。这样,当你的应用或数据库需要扩展时,可以轻松迁移到其他数据库管理系统,而不需要重写所有那些临时的函数。
在Python中使用原生的datetime对象,可以让你使用很多免费的、经过良好测试的、非EEE(拥抱、扩展、消灭)API。SQLAlchemy就是其中之一。
你知道在Mac和Windows中Excel的日期时间浮点数之间有一个微妙但危险的区别吗?谁知道将来你的某个客户会不会从Mac提交一个Excel文件,导致你的应用崩溃(更糟糕的是,他们可能因为这个错误突然赚了一百万美元)?
所以我建议你在用Python处理Excel时使用xlrd/xlwt,让SQLAlchemy和你的数据库使用标准的日期时间类型。不过,如果你坚持继续将日期时间存储为Excel浮点数,重用xlrd/xlwt中的代码可以节省你很多时间。它有将Python对象转换为Excel数据及反向转换的函数。
编辑:为了更清楚...
你从数据库读取到Python没有问题,因为你有一个类可以将浮点数转换为Python的datetime。
你在通过SQLAlchemy写入数据库或使用其他原生Python函数/模块/扩展时会遇到问题,因为你试图强行使用非标准类型,而它们期望的是标准的Python datetime。从Python的角度来看,Excel的日期时间类型是浮点数,而不是datetime。
虽然Python使用动态/鸭子类型,但它仍然是强类型的。它不会允许你做一些“无意义的事情”,比如将整数与字符串相加,或者强行将浮点数当作日期时间。
解决这个问题至少有两种方法:
声明一个自定义类型——这似乎是你想走的路。不幸的是,这是一条困难的道路。创建一个可以假装是datetime的浮点数类型相当困难。虽然可能,但需要大量的类型研究。抱歉,你得自己去理解相关文档。
创建工具函数——在我看来,这应该是更简单的方法。你需要两个函数:a) float_to_datetime(),用于将数据库中的数据转换为Python的datetime;b) datetime_to_float(),用于将Python的datetime转换为Excel的浮点数。
关于解决方案#2,正如我所说的,你可以通过重用xldate_from_datetime_tuple()来简化你的生活,这个函数来自xlrd/xlwt。这个函数“将一个日期时间元组(年、月、日、小时、分钟、秒)转换为Excel日期值。”安装xlrd后,去/path_to_python/lib/site-packages/xlrd。这个函数在xldate.py中,源代码有很好的文档说明,便于理解。