2016-07-13 2 views
3

Я использую SQLAlchemy для работы с удаленной базой данных, которая использует странный формат timestamp - она ​​хранит временные метки как миллисекунды с двойной точностью с эпохи. Я хотел бы работать с объектами питона даты и времени, поэтому я написал методы геттер/сеттер в моей модели, следуя this gist:Преобразование даты и времени в unix timestamp в SQLAlchemy модели перед выполнением запроса?

from sqlalchemy.ext.declarative import declarative_base 
from sqlalchemy.orm import synonym 
from sqlalchemy.dialects.mysql import DOUBLE 
import datetime 

Base = declarative_base() 
class Table(Base): 
    __tablename__ = "table" 

    id = Column(Integer, primary_key=True) 
    _timestamp = Column("timestamp", DOUBLE(asdecimal=False)) 

    @property 
    def timestamp(self): 
     return datetime.datetime.utcfromtimestamp(float(self._timestamp)/1000.) 

    @timestamp.setter 
    def timestamp(self, dt): 
     self._timestamp = float(dt.strftime("%s"))*1000. 

    timestamp = synonym('_timestamp', descriptor=timestamp) 

Это прекрасно работает для вставки новых строк в таблицу и работать с объектами из таблицы:

>>> table = session.query(Table).first() 
<Table id=1> 
>>> table.timestamp 
datetime.datetime(2016, 6, 27, 16, 9, 3, 320000) 
>>> table._timestamp 
1467043743320.0 

Однако, это ломается, когда я пытаюсь использовать DateTime в выражении фильтра:

>>> july = datetime.datetime(2016, 7, 1) 
>>> old = session.query(Table).filter(Table.timestamp < july).first() 
/lib/python2.7/site-packages/sqlalchemy/engine/default.py:450: Warning: Truncated incorrect DOUBLE value: '2016-07-01 00:00:00' 
>>> july_flt = float(july.strftime("%s"))*1000. 
>>> old = session.query(Table).filter(Table.timestamp < july_flt).first() 
<Table id=1> 

Я предполагаю, что это потому, что мои геттер/сеттер методы применяются т o экземпляры класса таблицы, но не изменяйте поведение самого класса. Я попытался переписывание с использованием гибридного свойства вместо синонима:

from sqlalchemy.ext.hybrid import hybrid_property 

class Table(Base): 
    __tablename__ = "table" 

    id = Column(Integer, primary_key=True) 
    _timestamp = Column("timestamp", DOUBLE(asdecimal=False)) 

    @hybrid_property 
    def timestamp(self): 
     return datetime.datetime.utcfromtimestamp(float(self._timestamp)/1000.) 

    @timestamp.setter 
    def timestamp(self, dt): 
     self._timestamp = float(dt.strftime("%s"))*1000. 

Опять же, это работает с экземплярами таблицы, но терпит неудача в запросе - теперь он ударился геттером, когда я запускаю запрос:

>>> july = datetime.datetime(2016, 7, 1) 
>>> old = session.query(Table).filter(Table.timestamp < july).first() 
Traceback: 
    File "models.py", line 42, in timestamp 
    return datetime.datetime.utcfromtimestamp(float(self._timestamp)/1000.) 
TypeError: float() argument must be a string or a number 

С отладчиком я вижу, что получатель получает класс Table._timestamp (а не конкретный Table._timestamp, а не 'июль).

Я вижу, что я мог бы использовать декоратор hybrid_property.expression, чтобы определить выражение SQL для преобразования временных меток в datetime, но мне бы очень хотелось, чтобы преобразовать дату и время в метку времени на стороне python, а затем запустить запрос с использованием временных меток , Другими словами, я хотел бы использовать datetime везде (в том числе в запросах), но все это делается с микросекундными метками времени на стороне SQL. Как я могу это сделать?

ответ

3

Вы должны использовать нестандартный тип, который не так страшен, как кажется.

from sqlalchemy.types import TypeDecorator 


class DoubleTimestamp(TypeDecorator): 
    impl = DOUBLE 

    def __init__(self): 
     TypeDecorator.__init__(self, as_decimal=False) 

    def process_bind_param(self, value, dialect): 
     return value.replace(tzinfo=datetime.timezone.utc).timestamp() * 1000 

    def process_result_value(self, value, dialect): 
     return datetime.datetime.utcfromtimestamp(value/1000) 

Table Тогда становится:

class Table(Base): 
    __tablename__ = "table" 

    id = Column(Integer, primary_key=True) 
    timestamp = Column(DoubleTimestamp) 

И тогда все, что вы уже работает. Вы вставляете, выбираете и сравниваете с datetime s, но хранится как DOUBLE.

Здесь я использовал другую логику для преобразования временных меток с strftime('%s') - это неправильное решение. Это другой вопрос, который был answered correctly here. О, и я заметил, что вы сказали микросекунды, но только конвертируете в миллисекунды в код, который вы отправили, если только это не было пропуском языка.

+0

Спасибо! Я попробую это сегодня днем ​​и приму ответ, когда он сработает. Re: Мили против микро, вы правы; это ошибка в документах программиста базы данных, которые я перенес в почту. Re: strftime, к сожалению, я использую Python 2.7 и не получаю метод 'datetime.timestamp()'. Метод «strftime» - это мой предпочтительный kludge, если я только запускаю его на своей локальной машине :) – nrlakin

+0

Решение 2.7 (dt - datetime (1970, 1, 1))/timedelta (seconds = 1) 'достаточно простое тоже :) – RazerM

+0

Да, но это так много. Если я пишу что-то для сервера, где я не знаю, на какой платформе он будет работать, я буду использовать что-то более безопасное, чем 'strftime'. Странно, что все из 2.7 решений для общей проблемы настолько неэлегантные ... – nrlakin

Смежные вопросы