如何在Django中添加沒(méi)有微秒的 DateTimeField 屬性詳解
前言
今天在項(xiàng)目中遇到一個(gè)Django的大坑,一個(gè)很簡(jiǎn)單的分頁(yè)問(wèn)題,造成了數(shù)據(jù)重復(fù)。最后排查發(fā)現(xiàn)是DateTimeField 屬性引起的。
下面描述下問(wèn)題,下面是我需要用到的一個(gè) Task Model 基本定義:
class Task(models.Model): # ...... 省略了其他字段 title = models.CharField(max_length=256, verbose_name=u'標(biāo)題') created_at = models.DateTimeField(auto_now_add=True, verbose_name=u'創(chuàng)建時(shí)間')
問(wèn)題描述
前端這邊的分頁(yè)方式不是常規(guī)的 page、page_size 方式,而是使用標(biāo)志位的方式進(jìn)行分頁(yè),我這里采用的就是通過(guò)創(chuàng)建時(shí)間的時(shí)間戳作為分頁(yè)標(biāo)記。比如下面是返回的第一頁(yè)的數(shù)據(jù):
{ "data": { "count": 5, "has_next": 1, "tasks": [ { "title": "這是一個(gè)作業(yè)標(biāo)題1", "ts": 1546829224000, "id": 1 }, { "title": "這是一個(gè)作業(yè)標(biāo)題2", "ts": 1546829641000, "id": 2 } ] }, "result": 1 }
要請(qǐng)求第2頁(yè)的數(shù)據(jù)只需要在請(qǐng)求的 API 中傳遞上一頁(yè)最后一條數(shù)據(jù)的時(shí)間戳即可,這里我們就傳遞 1546829641000,這樣當(dāng)我后臺(tái)接收到這個(gè)值過(guò)后就直接過(guò)濾大于該時(shí)間戳的數(shù)據(jù),再取一頁(yè)數(shù)據(jù)返回前端即可,邏輯上很簡(jiǎn)單。過(guò)濾核心代碼如下:
ts = string_utils.get_num(request.GET.get('ts', 0), 0) alltask = Task.objects.filter(created_at__gt=date_utils.timestamp2datetime(ts))
這段代碼很簡(jiǎn)單,主要就是將前臺(tái)傳遞過(guò)來(lái)的時(shí)間戳轉(zhuǎn)換成 DateTime 類型的數(shù)據(jù),然后利用created_at__gt來(lái)過(guò)濾,就是大于這個(gè)時(shí)間點(diǎn)的就可以。然后問(wèn)題來(lái)了,查詢出來(lái)的數(shù)據(jù)始終包含了上一頁(yè)最后一條數(shù)據(jù),感覺(jué)很奇怪,我這里明明用的是gt而不是gte,怎么會(huì)重復(fù)這條數(shù)據(jù)呢。
于是,我們把上一頁(yè)最后一條數(shù)據(jù)的 created_at 字段打印出來(lái)和傳遞過(guò)來(lái)的時(shí)間戳進(jìn)行對(duì)比下:
>>> task = Task.objects.get(pk=2) >>> task.created_at datetime.datetime(2019, 1, 7, 10, 54, 1, 343136)
然后將時(shí)間戳轉(zhuǎn)換成 DateTime 類型的數(shù)據(jù):
>>> ts = int(1546829641000/1000) >>> date_utils.timestamp2datetime(ts) datetime.datetime(2019, 1, 7, 10, 54, 1)
現(xiàn)在看到區(qū)別沒(méi)有,從數(shù)據(jù)庫(kù)中查詢出來(lái)的 created_at 字段的值包含了一個(gè)微秒,就是后面的 343136,而時(shí)間戳轉(zhuǎn)換成 DateTime 類型的值是不包含這個(gè)微秒值的,所以我們上面查詢的使用created_at__gt來(lái)進(jìn)行過(guò)濾很顯然 created_at 的值是大于下面的值的,因?yàn)槎嗔艘粋€(gè)微秒,所以就造成了數(shù)據(jù)重復(fù)了,終于破案了。
解決方法
那么要怎么解決這個(gè)問(wèn)題呢?當(dāng)然我們可以直接在數(shù)據(jù)庫(kù)中就保存一個(gè)時(shí)間戳的字段,用這個(gè)字段直接來(lái)進(jìn)行查詢過(guò)濾,肯定是可以解決這個(gè)問(wèn)題的。
如果就用現(xiàn)在的 created_at 這個(gè) DateTimeField 類型呢?如果保存的數(shù)據(jù)沒(méi)有這個(gè)微秒是不是也可以解決這個(gè)問(wèn)題啊?
我們可以去查看下源碼為什么 DateTimeField 類型的數(shù)據(jù)會(huì)包含微秒,下面是django/db/backends/mysql/base.py文件中的部分代碼說(shuō)明:
class DatabaseWrapper(BaseDatabaseWrapper): vendor = 'mysql' # This dictionary maps Field objects to their associated MySQL column # types, as strings. Column-type strings can contain format strings; they'll # be interpolated against the values of Field.__dict__ before being output. # If a column type is set to None, it won't be included in the output. _data_types = { 'AutoField': 'integer AUTO_INCREMENT', 'BinaryField': 'longblob', 'BooleanField': 'bool', 'CharField': 'varchar(%(max_length)s)', 'CommaSeparatedIntegerField': 'varchar(%(max_length)s)', 'DateField': 'date', 'DateTimeField': 'datetime', 'DecimalField': 'numeric(%(max_digits)s, %(decimal_places)s)', 'DurationField': 'bigint', 'FileField': 'varchar(%(max_length)s)', 'FilePathField': 'varchar(%(max_length)s)', 'FloatField': 'double precision', 'IntegerField': 'integer', 'BigIntegerField': 'bigint', 'IPAddressField': 'char(15)', 'GenericIPAddressField': 'char(39)', 'NullBooleanField': 'bool', 'OneToOneField': 'integer', 'PositiveIntegerField': 'integer UNSIGNED', 'PositiveSmallIntegerField': 'smallint UNSIGNED', 'SlugField': 'varchar(%(max_length)s)', 'SmallIntegerField': 'smallint', 'TextField': 'longtext', 'TimeField': 'time', 'UUIDField': 'char(32)', } @cached_property def data_types(self): if self.features.supports_microsecond_precision: return dict(self._data_types, DateTimeField='datetime(6)', TimeField='time(6)') else: return self._data_types # ... further class methods
上面的 data_types 方法中在進(jìn)行 MySQL 版本檢查,屬性supports_microsecond_precision來(lái)自于文件django/db/backends/mysql/features.py:
class DatabaseFeatures(BaseDatabaseFeatures): # ... properties and methods def supports_microsecond_precision(self): # See https://github.com/farcepest/MySQLdb1/issues/24 for the reason # about requiring MySQLdb 1.2.5 return self.connection.mysql_version >= (5, 6, 4) and Database.version_info >= (1, 2, 5)
從上面代碼可以看出如果使用的 MySQL 大于等于 5.6.4 版本,屬性DateTimeField會(huì)被映射成為數(shù)據(jù)庫(kù)中的datetime(6),所以保存的數(shù)據(jù)就包含了微秒。
在 Django 中暫時(shí)沒(méi)有發(fā)現(xiàn)可以針對(duì)改配置進(jìn)行設(shè)置的方法,所以我們要想保存的數(shù)據(jù)不包含微秒,我們這里則可以將上面的data_types屬性進(jìn)行覆蓋即可:
from django.db.backends.mysql.base import DatabaseWrapper DatabaseWrapper.data_types = DatabaseWrapper._data_types
將上面的代碼放置在合適的地方,比如models.py或者_(dá)_init__.py或者其他地方,當(dāng)我們運(yùn)行 migrations 命令來(lái)創(chuàng)建 DateTimeField 列的時(shí)候?qū)?yīng)在數(shù)據(jù)庫(kù)中的字段就被隱射成為了datetime,而不是datetime(6),即使你用的是 5.6.4 版本以上的數(shù)據(jù)庫(kù)。
當(dāng)然要立即解決當(dāng)前的問(wèn)題,只需要更改下數(shù)據(jù)庫(kù)中的 created_at 字段的類型即可:
mysql> ALTER TABLE `task` CHANGE COLUMN `created_at` `created_at` datetime NOT NULL; Query OK, 156 rows affected (0.14 sec) Records: 156 Duplicates: 0 Warnings: 0
這樣數(shù)據(jù)重復(fù)的 BUG 就解決了。
參考鏈接:https://stackoverflow.com/questions/46539755/how-to-add-datetimefield-in-django-without-microsecond
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問(wèn)大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
基于python SMTP實(shí)現(xiàn)自動(dòng)發(fā)送郵件教程解析
這篇文章主要介紹了基于python實(shí)現(xiàn)自動(dòng)發(fā)送郵件教程解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-06-06用Python實(shí)現(xiàn)QQ游戲大家來(lái)找茬輔助工具
這是一個(gè)用于QQ大家來(lái)找茬(美女找茬)的輔助外掛,開(kāi)發(fā)的原因是看到老爸天天在玩這個(gè)游戲,分?jǐn)?shù)是慘不忍睹的負(fù)4000多。本來(lái)是想寫個(gè)很簡(jiǎn)單的東西,但由于過(guò)程中老爸的多次嘲諷,逼得我不得不盡力完善,最后形成了一個(gè)小小的產(chǎn)品。2014-09-09python實(shí)現(xiàn)通過(guò)pil模塊對(duì)圖片格式進(jìn)行轉(zhuǎn)換的方法
這篇文章主要介紹了python實(shí)現(xiàn)通過(guò)pil模塊對(duì)圖片格式進(jìn)行轉(zhuǎn)換的方法,涉及Python中pil模塊的使用技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-03-03如何將你的應(yīng)用遷移到Python3的三個(gè)步驟
這篇文章主要介紹了如何將你的應(yīng)用遷移到Python3的三個(gè)步驟,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12python使用多線程備份數(shù)據(jù)庫(kù)的步驟
在日常服務(wù)器運(yùn)維工作中,備份數(shù)據(jù)庫(kù)是必不可少的,剛工作那會(huì)看到公司都是用shell腳本循環(huán)備份數(shù)據(jù)庫(kù),到現(xiàn)在自己學(xué)習(xí)python語(yǔ)言后,利用多進(jìn)程多線程相關(guān)技術(shù)來(lái)實(shí)現(xiàn)并行備份數(shù)據(jù)庫(kù),充分利用服務(wù)器資源,提高備份速度。2021-05-05Python操作Elasticsearch處理timeout超時(shí)
這篇文章主要介紹了Python操作Elasticsearch處理timeout超時(shí),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-07-07pytorch實(shí)現(xiàn)onehot編碼轉(zhuǎn)為普通label標(biāo)簽
今天小編就為大家分享一篇pytorch實(shí)現(xiàn)onehot編碼轉(zhuǎn)為普通label標(biāo)簽,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-01-01Django?Rest?Framework實(shí)現(xiàn)身份認(rèn)證源碼詳解
這篇文章主要為大家介紹了Django?Rest?Framework實(shí)現(xiàn)身份認(rèn)證源碼詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-05-05Python設(shè)計(jì)模式之命令模式簡(jiǎn)單示例
這篇文章主要介紹了Python設(shè)計(jì)模式之命令模式,簡(jiǎn)單介紹了命令模式的概念、原理,并結(jié)合實(shí)例形式分析了Python命令模式的定義與使用相關(guān)操作技巧,需要的朋友可以參考下2018-01-01