Как бороться с «частичными» датами (2010-00-00) от MySQL в Django?

В одном из моих проектов Django, которые используют MySQL в качестве базы данных, мне нужно иметь поля даты, которые принимают также «частичные» даты, такие как только год (ГГГГ), год и месяц (ГГГГ-ММ) плюс нормальная дата (ГГГГ-ММ- DD).

Поле даты в MySQL может справиться с этим, приняв 00 за месяц и день. Таким образом, 2010-00-00 действителен в MySQL и представляет собой 2010 год. То же самое и для 2010-05-00, которые представлены в мае 2010 года.

Поэтому я начал создавать PartialDateField для поддержки этой функции. Но я ударил стену, потому что по умолчанию и Django используют по умолчанию MySQLdb, драйвер python для MySQL, возвращают объект datetime.date для поля даты. И datetime.date() поддерживает только реальную дату. Таким образом, можно изменить конвертер для поля даты, используемого MySQLdb, и вернуть только строку в этом формате «YYYY-MM-DD» . К сожалению, использование конвертера MySQLdb установлено на уровне соединения, поэтому оно используется для всех полей даты MySQL. Но Django DateField полагается на то, что база данных возвращает объект datetime.date , поэтому, если я изменю конвертер, чтобы вернуть строку, Django вообще не нравится.

У кого-то есть идея или совет для решения этой проблемы? Как создать PartialDateField в Django?

РЕДАКТИРОВАТЬ

Также я должен добавить, что я уже думал о 2 решениях, создавал 3 целочисленных поля в течение года, месяца и дня (как упоминается Алисоном Р. ) или использовал поле varchar для сохранения даты как строки в этом формате YYYY-MM-DD .

Но в обоих решениях, если я не ошибаюсь, я потеряю специальные свойства поля даты, например, сделаю для них такой запрос: Получите все записи после этой даты . Я могу, вероятно, повторно реализовать эту функцию на стороне клиента, но это не будет правильным решением в моем случае, потому что база данных может быть запросом из других систем (клиент mysql, MS Access и т. Д.),

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

Поэтому моим окончательным решением на стороне БД является использование поля varchar (ограничено 10 символами) и сохранение даты в нем в виде строки в формате ISO (YYYY-MM-DD) с 00 в течение месяца и дня когда нет месяца и / или дня (например, поля даты в MySQL). Таким образом, это поле может работать с любыми базами данных, данные могут быть прочитаны, поняты и отредактированы напрямую и легко человеком с помощью простого клиента (например, mysql-клиент, phpmyadmin и т. Д.). Это было требованием. Он также может быть экспортирован в Excel / CSV без какого-либо преобразования и т. Д. Недостатком является то, что формат не применяется (кроме Django). Кто-то может написать «не дату» или сделать ошибку в формате, и БД примет ее (если у вас есть идея об этой проблеме …).

Таким образом, можно также делать все специальные запросы поля даты относительно легко. Для запросов с WHERE: <,>, <=,> = и = работают напрямую. Запросы IN и BETWEEN работают напрямую. Для запросов по дням или месяцам вам просто нужно сделать это с помощью EXTRACT (DAY | MONTH …). Заказ работы также напрямую. Поэтому я думаю, что он охватывает все запросы и, в основном, не усложняет.

На стороне Django я сделал 2 вещи. Во-первых, я создал объект PartialDate который выглядит в основном как datetime.date но поддерживает дату без месяца и / или дня. Внутри этого объекта я использую объект datetime.datetime, чтобы сохранить дату. Я использую часы и минуты как флаг, указывающий, действительны ли месяц и день, когда они установлены на 1. Это та же идея, что и Steveha, но с другой реализацией (и только на стороне клиента). Использование объекта datetime.datetime дает мне много приятных функций для работы с датами (валидация, сопоставление и т. Д.).

Во-вторых, я создал PartialDateField который в основном занимается конверсией между объектом PartialDate и базой данных.

Пока это работает очень хорошо (я в основном заканчиваю свои обширные модульные тесты).

Вы можете сохранить неполную дату в виде целого числа (желательно в поле, названном для той части даты, которую вы храните, например year, month или day ), и выполнить проверку и преобразование в объект даты в модели.

РЕДАКТИРОВАТЬ

Если вам нужны реальные функциональные возможности, вам, вероятно, нужны реальные, а не частичные даты. Например, «получают ли все после 2010-0-0» даты возврата, включая 2010 год или только даты в 2011 году и далее? То же самое касается и вашего другого примера мая 2010 года. Способы, с помощью которых разные языки / клиенты имеют дело с частичными датами (если они их вообще поддерживают), скорее всего, будут очень своеобразными, и вряд ли они будут соответствовать реализации MySQL.

С другой стороны, если вы сохраняете целое число года, например 2010, легко спросить базу данных «все записи с годом> 2010» и точно понять, каким должен быть результат от любого клиента на любой платформе. Вы можете даже комбинировать этот подход для более сложных дат / запросов, таких как «все записи с годом> 2010 и месяц> 5».

ВТОРОЙ РЕДАКТИРОВАНИЕ

Ваш единственный (и, возможно, лучший) вариант заключается в том, чтобы хранить действительно достоверные даты и придумать соглашение в вашем приложении для того, что они означают. Поле DATETIME с именем date_month может иметь значение 2010-05-01, но вы бы рассматривали это как представление всех дат в мае 2010 года. При программировании вам нужно будет это учитывать. Если у вас был date_month в Python как объект datetime, вам нужно будет вызвать функцию date_month.end_of_month() для запроса дат, следующих за этим месяцем. (Это псевдокод, но его можно легко реализовать с помощью чего-то вроде модуля календаря ).

Похоже, вы хотите сохранить интервал дат. В Python это было бы (с моим пониманием по-прежнему-немного) было бы легко реализовать, сохранив два объекта datetime.datetime, один из которых указывает начало диапазона дат, а другой – конец. Подобным образом, который используется для указания фрагментов списка, конечная точка сама не будет включена в диапазон дат.

Например, этот код будет применять диапазон дат в качестве именованного кортежа:

 >>> from datetime import datetime >>> from collections import namedtuple >>> DateRange = namedtuple('DateRange', 'start end') >>> the_year_2010 = DateRange(datetime(2010, 1, 1), datetime(2011, 1, 1)) >>> the_year_2010.start <= datetime(2010, 4, 20) < the_year_2010.end True >>> the_year_2010.start <= datetime(2009, 12, 31) < the_year_2010.end False >>> the_year_2010.start <= datetime(2011, 1, 1) < the_year_2010.end False 

Или даже добавить магию:

 >>> DateRange.__contains__ = lambda self, x: self.start <= x < self.end >>> datetime(2010, 4, 20) in the_year_2010 True >>> datetime(2011, 4, 20) in the_year_2010 False 

Это такая полезная концепция, что я уверен, что кто-то уже сделал реализацию доступной. Например, быстрый взгляд предполагает, что класс relativedate из пакета dateutil сделает это и более выразительно, разрешив передать аргумент ключевого слова 'years' для конструктора.

Однако сопоставление такого объекта в полях базы данных несколько сложнее, поэтому вам может быть лучше реализовать его просто, просто потянув оба поля отдельно, а затем объединив их. Я полагаю, это зависит от структуры БД; Я еще не очень хорошо знаком с этим аспектом Python.

В любом случае, я думаю, что ключ должен думать о «частичной дате» как о диапазоне, а не как о простом значении.

редактировать

Это заманчиво, но я считаю неуместным добавлять более магические методы, которые будут обрабатывать использование операторов > и < . Там есть немного двусмысленности: есть ли дата, «больше чем» заданного диапазона, после окончания диапазона или после его начала? Первоначально кажется целесообразным использовать <= чтобы указать, что дата в правой части уравнения находится после начала диапазона, и < чтобы указать, что это после конца.

Однако это подразумевает равенство между диапазоном и датой внутри диапазона, что неверно, поскольку это означает, что месяц мая 2010 года соответствует 2010 году, так как 4 мая 2010 года соответствует их обоим. IE, вы в конечном итоге оказались бы с фальсификациями вроде 2010-04-20 == 2010 == 2010-05-04 .

Поэтому, вероятно, было бы лучше реализовать такой метод, как isafterstart чтобы явно проверить, соответствует ли дата после начала диапазона. Но опять же, возможно, кто-то уже сделал это, поэтому, вероятно, стоит посмотреть на pypi, чтобы увидеть, что считается готовым к производству. Об этом свидетельствует наличие «статуса разработки :: 5 – производство / стабильный» в разделе «Категории» страницы pypi данного модуля. Обратите внимание, что не всем модулям присвоен статус развития.

Или вы можете просто сохранить его простым и использовать базовую реализацию namedtuple, явно проверить

 >>> datetime(2012, 12, 21) >= the_year_2010.start True 

Можете ли вы сохранить дату вместе с флагом, который сообщает, сколько даты действительна?

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

 YEAR_VALID = 0x04 MONTH_VALID = 0x02 DAY_VALID = 0x01 Y_VALID = YEAR_VALID YM_VALID = YEAR_VALID | MONTH_VALID YMD_VALID = YEAR_VALID | MONTH_VALID | DAY_VALID 

Затем, если у вас есть дата, такая как 2010-00-00, конвертируйте ее в 2010-01-01 и установите флаг в Y_VALID. Если у вас есть дата 2010-06-00, конвертируйте ее в 2010-06-01 и установите флаг YM_VALID.

Итак, PartialDateField будет классом, который объединяет дату и флаг даты, описанные выше.

PS Вам действительно не нужно использовать флаги так, как я показал это; это старый программист С во мне, выходящий на поверхность. Вы можете использовать Y_VALID, YM_VALID, YMD_VALID = диапазон (3), и это тоже сработает. Ключ должен иметь какой-то флаг, который сообщает вам, сколько времени доверять.

Хотя и не в Python – вот пример того, как одна и та же проблема была решена в Ruby – с использованием единственного значения Integer – и побитовых операторов для хранения года, месяца и дня – с дополнительным месяцем и днем.

https://github.com/58bits/partial-date

Посмотрите на источник в lib для date.rb и bits.rb.

Я уверен, что аналогичное решение может быть написано на Python.

Чтобы сохранить дату (сортировать), вы просто сохраняете Integer в базе данных.