如何在Django中处理MySQL的“部分”日期(2010-00-00)?
在我的一个Django项目中,我使用MySQL作为数据库。我需要一个日期字段,能够接受“部分”日期,比如只输入年份(YYYY)或者年份和月份(YYYY-MM),当然也包括正常的日期格式(YYYY-MM-DD)。
在MySQL中,日期字段可以通过接受00作为月份和日期来处理这种情况。所以2010-00-00在MySQL中是有效的,它代表的是2010年。同样,2010-05-00代表的是2010年5月。
因此,我开始创建一个PartialDateField
来支持这个功能。但我遇到了一个问题,因为默认情况下,Django使用的MySQLdb这个Python驱动会为日期字段返回一个datetime.date
对象,而datetime.date()
只支持真实的日期。所以我可以修改MySQLdb为日期字段使用的转换器,让它只返回格式为'YYYY-MM-DD'的字符串。不幸的是,MySQLdb使用的转换器是在连接级别设置的,所以它会影响所有MySQL的日期字段。但Django的DateField
依赖于数据库返回一个datetime.date
对象,因此如果我把转换器改成返回字符串,Django就会不高兴。
有没有人有什么想法或建议来解决这个问题?如何在Django中创建一个PartialDateField
?
编辑
我还应该补充一下,我已经考虑了两种解决方案,创建三个整数字段来分别存储年份、月份和日期(正如Alison R.提到的)或者使用一个varchar字段将日期以字符串的形式存储,格式为YYYY-MM-DD。
但在这两种解决方案中,如果我没记错的话,我会失去日期字段的一些“特殊”属性,比如可以对它们进行这样的查询:获取所有在这个日期之后的条目。我可能可以在客户端重新实现这个功能,但在我的情况下这并不是一个有效的解决方案,因为数据库可能会被其他系统(如MySQL客户端、MS Access等)查询。
5 个回答
听起来你想要存储一个日期区间。在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年5月等于2010年,因为2010年5月4日同时对应这两者。也就是说,你可能会得到像2010-04-20 == 2010 == 2010-05-04
这样的错误结果。
所以,可能更好的做法是实现一个像isafterstart
的方法,明确检查一个日期是否在范围的开始之后。但再说一次,可能已经有人做过了,所以在pypi上看看哪些是被认为是生产就绪的实现是值得的。一个模块的pypi页面的“类别”部分如果有“Development Status :: 5 - Production/Stable”,就表示它是稳定的。注意,并不是所有模块都有开发状态。
或者你可以简单点,使用基本的命名元组实现,明确检查
>>> datetime(2012, 12, 21) >= the_year_2010.start
True
你可以把部分日期存储为一个整数(最好是在一个以你存储的日期部分命名的字段里,比如 year
、month
或 day
),然后在模型中进行验证和转换成日期对象。
编辑
如果你需要真正的日期功能,那你可能需要完整的日期,而不是部分日期。例如,“获取2010-0-0之后的所有内容”是包括2010年的日期,还是只包括2011年及以后的日期?对于你提到的2010年5月也是一样。不同的编程语言或客户端处理部分日期的方式(如果它们支持的话)可能各不相同,而且很可能和MySQL的实现不一致。
另一方面,如果你把 year
存储为像2010这样的整数,就很容易向数据库请求“所有年份大于2010的记录”,并且可以清楚地理解结果应该是什么,无论是从哪个客户端,在哪个平台上。你甚至可以把这种方法结合起来,处理更复杂的日期或查询,比如“所有年份大于2010且月份大于5的记录”。
第二次编辑
你唯一的其他选择(也许是最好的选择)是存储真正有效的日期,并在你的应用程序中制定一个约定来说明它们的含义。一个名为 date_month
的DATETIME字段可以有一个值为2010-05-01,但你会把它视为代表2010年5月的所有日期。在编程时你需要考虑这一点。如果你在Python中有一个 date_month
的datetime对象,你需要调用一个像 date_month.end_of_month()
的函数来查询该月份之后的日期。(这只是伪代码,但可以很容易地用像calendar模块实现。)
首先,感谢大家的回答。虽然没有一个答案完全解决我的问题,但为了公平起见,我得说我没有提供所有的需求。不过,你们每个人的回答都让我思考了我的问题,有些想法最终成了我的解决方案的一部分。
所以,我的最终解决方案是在数据库那边使用一个 varchar 字段(限制为10个字符),把日期以字符串的形式存储在里面,采用ISO格式(YYYY-MM-DD),当没有月份和/或日期时,用 00 来表示(就像MySQL中的 date 字段)。这样,这个字段可以在任何数据库中使用,数据可以被人直接用简单的客户端(比如mysql客户端、phpmyadmin等)读取、理解和编辑。这是一个需求。它也可以直接导出到Excel/CSV,而不需要任何转换等。缺点是格式没有强制要求(除了在Django中)。有人可能会写 'not a date' 或者在格式上犯错误,而数据库会接受这些(如果你对这个问题有了解的话...)。
这样做也能相对容易地进行所有 date 字段的 特殊 查询。对于带有WHERE的查询:<, >, <=, >= 和 = 都可以直接使用。IN 和 BETWEEN 查询也可以直接使用。按天或按月查询时,只需要用EXTRACT (DAY|MONTH ...) 就可以了。排序也可以直接进行。所以我认为这能满足所有查询需求,而且基本没有复杂性。
在Django那边,我做了两件事。首先,我创建了一个 PartialDate
对象,它看起来和 datetime.date
很像,但支持没有月份和/或日期的日期。在这个对象内部,我使用了一个 datetime.datetime 对象来保存日期。我用小时和分钟作为标志,告诉我当它们被设置为1时,月份和日期是有效的。这是 steveha 提出的相同思路,但实现方式不同(而且只在客户端)。使用 datetime.datetime
对象让我在处理日期时有很多不错的功能(验证、比较等)。
其次,我创建了一个 PartialDateField
,主要处理 PartialDate
对象和数据库之间的转换。
到目前为止,这个方案运行得相当不错(我已经基本完成了我的全面单元测试)。