【问题标题】:How to deal with "partial" dates (2010-00-00) from MySQL in Django?如何在 Django 中处理来自 MySQL 的“部分”日期(2010-00-00)?
【发布时间】:2010-06-04 02:49:07
【问题描述】:

在我的一个使用 MySQL 作为数据库的 Django 项目中,我需要有一个 date 字段,该字段也接受“部分”日期,例如仅年份 (YYYY) 和年份和月份 (YYYY- MM) 加上正常日期 (YYYY-MM-DD)。

MySQL 中的 date 字段可以通过接受 00 来处理月份和日期。所以 2010-00-00 在 MySQL 中是有效的,它代表 2010。2010-05-00 代表 2010 年 5 月。

所以我开始创建一个PartialDateField 来支持这个功能。但我碰壁了,因为默认情况下,Django 使用默认的 MySQLdb,即 MySQL 的 python 驱动程序,为 date 字段返回一个datetime.date 对象,并且datetime.date() 仅支持真实日期。因此可以修改 MySQLdb 使用的 date 字段的转换器,并仅返回此格式 'YYYY-MM-DD' 的字符串。不幸的是 MySQLdb 使用的转换器设置在连接级别,因此它用于所有 MySQL date 字段。但是 Django DateField 依赖于数据库返回一个 datetime.date 对象这一事实,所以如果我将转换器更改为返回一个字符串,Django 一点也不高兴。

有人有解决这个问题的想法或建议吗?如何在 Django 中创建PartialDateField

编辑

另外我应该补充一点,我已经想到了 2 个解决方案,为年、月和日创建 3 个整数字段(如 Alison R. 所述)或使用 varchar 字段以这种格式将日期保存为字符串 YYYY-MM-DD.

但是在这两种解决方案中,如果我没记错的话,我将失去 date 字段的 special 属性,就像对它们进行此类查询一样:获取此日期之后的所有条目。我可能可以在客户端重新实现此功能,但在我的情况下这不是一个有效的解决方案,因为可以从其他系统(mysql 客户端、MS Access 等)查询数据库

【问题讨论】:

    标签: python mysql database django date


    【解决方案1】:

    首先,感谢您的所有回答。按原样,它们都不是解决我问题的好方法,但是,为了您的辩护,我应该补充一点,我没有给出所有要求。但每个人都帮助我思考我的问题,而你的一些想法是我最终解决方案的一部分。

    所以我在数据库方面的最终解决方案是使用 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 对象和数据库之间的转换。

    到目前为止,它工作得很好(我已经完成了我的大量单元测试)。

    【讨论】:

    • 这似乎是一个很好的解决方案。我不喜欢使用小时和分钟作为标志的技巧,因为我担心有一天你可能需要使用它们来实际存储小时和分钟,然后你就会遇到问题。但如果那一天永远不会到来,那我就什么都不担心了!
    • 如果您需要扩展它以处理小时、分钟、秒等。我建议您使用 RFC 3339 标准,因为它与您的解决方案具有相同的优点(适用于任何数据库,人类可读等)ietf.org/rfc/rfc3339.txt
    • 感谢您提供有关 RFC 的信息!我同意使用小时和分钟作为标志是我解决方案中唯一的黑客部分,但它使排序等事情变得如此简单。如果我将来需要时间,我将创建一个新的 PartialDateTime 对象(不是 PartialDate)另一个实现。但老实说,我很难看到这个用例?
    • 这里似乎是这种方法的一个实现:gitorious.org/wmbr-playback/wmbr-dj3000/source/…
    【解决方案2】:

    您可以将部分日期存储为整数(最好在以您存储的日期部分命名的字段中,例如 year, monthday)并进行验证和转换为日期对象在模型中。

    编辑

    如果您需要真正的日期功能,您可能需要真正的而不是部分的日期。例如,“获取 2010-0-0 之后的所有内容”返回日期是否包括 2010 年或仅返回 2011 年及以后的日期?您在 2010 年 5 月的另一个示例也是如此。不同语言/客户端处理部分日期的方式(如果它们完全支持的话)可能非常特殊,它们不太可能与 MySQL 的实现相匹配。

    另一方面,如果您存储一个year 整数,例如 2010,则很容易向数据库询问“年份 > 2010 的所有记录”,并从任何客户端准确了解结果应该是什么任何平台。您甚至可以将这种方法结合起来处理更复杂的日期/查询,例如“年 > 2010 年且月 > 5 的所有记录”。

    第二次编辑

    您唯一的其他(也许是最好的)选择是存储真正有效的日期,并在您的应用程序中为它们的含义制定一个约定。一个名为 date_month 的 DATETIME 字段的值可能是 2010-05-01,但您会将其视为表示 2010 年 5 月的所有日期。您需要在编程时适应这一点。如果您在 Python 中有 date_month 作为日期时间对象,则需要调用像 date_month.end_of_month() 这样的函数来查询该月之后的日期。 (这是伪代码,但可以使用 calendar 模块之类的东西轻松实现。)

    【讨论】:

    • 我已经想到了这个解决方案,但我认为它不适用于我的情况。请参阅我的编辑。
    【解决方案3】:

    听起来你想存储一个日期间隔。在 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
    

    这是一个非常有用的概念,我很确定有人已经提供了一个实现。例如,快速浏览一下就表明 dateutil 包中的 relativedate 类将执行此操作,并且更具有表现力的是,允许将“年”关键字参数传递给构造函数。

    但是,将这样的对象映射到数据库字段有点复杂,因此您最好通过分别拉出两个字段然后组合它们来实现它。我想这取决于数据库框架;我对 Python 的这方面还不是很熟悉。

    无论如何,我认为关键是将“部分日期”视为一个范围,而不是一个简单的值。

    编辑

    添加更多魔术方法来处理&gt;&lt; 运算符的使用很诱人,但我认为不合适。那里有点模棱两可:“大于”给定范围的日期是在范围结束之后还是开始之后出现?最初使用&lt;= 表示等式右侧的日期在范围开始之后似乎是合适的,而&lt; 表示它在结束之后。

    但是,这意味着范围和范围内的日期相等,这是不正确的,因为这意味着 2010 年 5 月等于 2010 年,因为 2010 年 5 月 4 日等于两者他们。 IE 你最终会得到像 2010-04-20 == 2010 == 2010-05-04 这样的虚假事实。

    所以可能最好实现像isafterstart 这样的方法来显式检查日期是否在范围的开头之后。但同样,有人可能已经这样做了,所以可能值得在pypi 上看看,看看什么是生产就绪的。这由给定模块的 pypi 页面的“类别”部分中的“开发状态 :: 5 - 生产/稳定”的存在表示。请注意,并非所有模块都已获得开发状态。

    或者你可以保持简单,并使用基本的命名元组实现,明确检查

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

    【讨论】:

    • 我绝对不存储时间间隔。这个 PartialDate 将主要用于存储人们的生日日期,无论是死的还是活着的。所以,经常,尤其是对于死去的人,我的客户没有完整的日期。但它有助于我的发展“将“部分日期”视为一个范围”。
    【解决方案4】:

    您能否将日期与一个标志一起存储,该标志告诉您有多少日期是有效的?

    类似这样的:

    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 将是一个将日期和上述日期有效标志捆绑在一起的类。

    附:您实际上不需要像我展示的那样使用标志;那是我的老 C 程序员浮出水面。您可以使用 Y_VALID、YM_VALID、YMD_VALID = range(3),它也可以正常工作。关键是要有某种标志,告诉您可以信任多少日期。

    【讨论】:

    • 这个答案没有解决 Python 不认为 2010-00-00 之类的有效日期(即使 MySQL 认为)这样的事实。你建议他如何存储和检索它?
    • 这个答案表明他将这样的日期转换为 2010-01-01。看,它就在那里,我不只是编辑它。 MySQL 有一个特性,你可以存储奇怪的部分日期,但 Python 不理解它们,所以我特别建议不要使用奇怪的部分日期,而是使用日期和一个标志,说明你实际上可以信任多少日期.您实际上不需要像我展示的那样使用标志;那是我的老 C 程序员浮出水面。您可以使用 Y_VALID、YM_VALID、YMD_VALID = range(3),它也可以正常工作。
    • 很公平。不幸的是,除非编辑了答案,否则我无法撤销我的反对票。 (P.S. 这确实看起来像是一个相当 C-ish 的解决方案;)
    【解决方案5】:

    虽然不是在 Python 中 - 这是一个在 Ruby 中如何解决相同问题的示例 - 使用单个整数值 - 和按位运算符存储年、月和日 - 月和日可选。

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

    在 lib 中查看 date.rb 和 bits.rb 的源代码。

    我确信可以用 Python 编写类似的解决方案。

    要保留日期(可排序),您只需将整数保存到数据库中。

    【讨论】:

      猜你喜欢
      • 2012-11-17
      • 2016-07-11
      • 2018-07-31
      • 2011-07-10
      • 2013-06-09
      • 1970-01-01
      • 2013-07-12
      • 2017-09-21
      • 2017-10-03
      相关资源
      最近更新 更多