如何在Django中添加没有微秒的 DateTimeField 属性详解


Posted in Python onJanuary 30, 2019

前言

今天在项目中遇到一个Django的大坑,一个很简单的分页问题,造成了数据重复。最后排查发现是DateTimeField 属性引起的。

下面描述下问题,下面是我需要用到的一个 Task Model 基本定义:

class Task(models.Model):
 # ...... 省略了其他字段
 title = models.CharField(max_length=256, verbose_name=u'标题')
 created_at = models.DateTimeField(auto_now_add=True, verbose_name=u'创建时间')

问题描述

前端这边的分页方式不是常规的 page、page_size 方式,而是使用标志位的方式进行分页,我这里采用的就是通过创建时间的时间戳作为分页标记。比如下面是返回的第一页的数据:

{
 "data": {
 "count": 5,
 "has_next": 1,
 "tasks": [
 {
 "title": "这是一个作业标题1",
 "ts": 1546829224000,
 "id": 1
 },
 {
 "title": "这是一个作业标题2",
 "ts": 1546829641000,
 "id": 2
 }
 ]
 },
 "result": 1
}

要请求第2页的数据只需要在请求的 API 中传递上一页最后一条数据的时间戳即可,这里我们就传递 1546829641000,这样当我后台接收到这个值过后就直接过滤大于该时间戳的数据,再取一页数据返回前端即可,逻辑上很简单。过滤核心代码如下:

ts = string_utils.get_num(request.GET.get('ts', 0), 0)
alltask = Task.objects.filter(created_at__gt=date_utils.timestamp2datetime(ts))

这段代码很简单,主要就是将前台传递过来的时间戳转换成 DateTime 类型的数据,然后利用created_at__gt来过滤,就是大于这个时间点的就可以。然后问题来了,查询出来的数据始终包含了上一页最后一条数据,感觉很奇怪,我这里明明用的是gt而不是gte,怎么会重复这条数据呢。

于是,我们把上一页最后一条数据的 created_at 字段打印出来和传递过来的时间戳进行对比下:

>>> task = Task.objects.get(pk=2)
>>> task.created_at
datetime.datetime(2019, 1, 7, 10, 54, 1, 343136)

然后将时间戳转换成 DateTime 类型的数据:

>>> ts = int(1546829641000/1000)
>>> date_utils.timestamp2datetime(ts)
datetime.datetime(2019, 1, 7, 10, 54, 1)

现在看到区别没有,从数据库中查询出来的 created_at 字段的值包含了一个微秒,就是后面的 343136,而时间戳转换成 DateTime 类型的值是不包含这个微秒值的,所以我们上面查询的使用created_at__gt来进行过滤很显然 created_at 的值是大于下面的值的,因为多了一个微秒,所以就造成了数据重复了,终于破案了。

解决方法

那么要怎么解决这个问题呢?当然我们可以直接在数据库中就保存一个时间戳的字段,用这个字段直接来进行查询过滤,肯定是可以解决这个问题的。

如果就用现在的 created_at 这个 DateTimeField 类型呢?如果保存的数据没有这个微秒是不是也可以解决这个问题啊?

我们可以去查看下源码为什么 DateTimeField 类型的数据会包含微秒,下面是django/db/backends/mysql/base.py文件中的部分代码说明:

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 方法中在进行 MySQL 版本检查,属性supports_microsecond_precision来自于文件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会被映射成为数据库中的datetime(6),所以保存的数据就包含了微秒。

在 Django 中暂时没有发现可以针对改配置进行设置的方法,所以我们要想保存的数据不包含微秒,我们这里则可以将上面的data_types属性进行覆盖即可:

from django.db.backends.mysql.base import DatabaseWrapper

DatabaseWrapper.data_types = DatabaseWrapper._data_types

将上面的代码放置在合适的地方,比如models.py或者__init__.py或者其他地方,当我们运行 migrations 命令来创建 DateTimeField 列的时候对应在数据库中的字段就被隐射成为了datetime,而不是datetime(6),即使你用的是 5.6.4 版本以上的数据库。

当然要立即解决当前的问题,只需要更改下数据库中的 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

这样数据重复的 BUG 就解决了。

参考链接:https://stackoverflow.com/questions/46539755/how-to-add-datetimefield-in-django-without-microsecond

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对三水点靠木的支持。

Python 相关文章推荐
python获取糗百图片代码实例
Dec 18 Python
零基础写python爬虫之urllib2中的两个重要概念:Openers和Handlers
Nov 05 Python
Python3.6实现连接mysql或mariadb的方法分析
May 18 Python
python 多线程中子线程和主线程相互通信方法
Nov 09 Python
django-allauth入门学习和使用详解
Jul 03 Python
详解将Pandas中的DataFrame类型转换成Numpy中array类型的三种方法
Jul 06 Python
Python OpenCV调用摄像头检测人脸并截图
Aug 20 Python
Python 通过微信控制实现app定位发送到个人服务器再转发微信服务器接收位置信息
Aug 05 Python
python聚类算法解决方案(rest接口/mpp数据库/json数据/下载图片及数据)
Aug 28 Python
tensorflow之获取tensor的shape作为max_pool的ksize实例
Jan 04 Python
Python实时监控网站浏览记录实现过程详解
Jul 14 Python
python中append函数用法讲解
Dec 11 Python
Pandas统计重复的列里面的值方法
Jan 30 #Python
自学python的建议和周期预算
Jan 30 #Python
python DataFrame 取差集实例
Jan 30 #Python
对python dataframe逻辑取值的方法详解
Jan 30 #Python
对Python中DataFrame选择某列值为XX的行实例详解
Jan 29 #Python
把pandas转换int型为str型的方法
Jan 29 #Python
使用pandas把某一列的字符值转换为数字的实例
Jan 29 #Python
You might like
ThinkPHP中实例Model方法的区别说明
2010/08/21 PHP
我的php学习笔记(毕业设计)
2012/02/21 PHP
解决laravel5中auth用户登录其他页面获取不到登录信息的问题
2019/10/08 PHP
PHP pthreads v3下worker和pool的使用方法示例
2020/02/21 PHP
JS类定义原型方法的两种实现的区别评论很多
2007/09/12 Javascript
一些mootools的学习资源
2010/02/07 Javascript
JavaScript具有类似Lambda表达式编程能力的代码(改进版)
2010/09/14 Javascript
导入extjs、jquery 文件时$使用冲突问题解决方法
2014/01/14 Javascript
调用DOM对象的focus使文本框获得焦点
2014/02/19 Javascript
详解JavaScript语法对{}处理的坑爹之处
2014/06/05 Javascript
jQuery实现带玻璃流光质感的手风琴特效
2015/11/20 Javascript
javascript先序遍历DOM树的方法
2016/02/27 Javascript
Vue.js报错Failed to resolve filter问题的解决方法
2016/05/25 Javascript
Vue+Express实现登录状态权限验证的示例代码
2019/05/05 Javascript
小程序扫描普通链接二维码跳转小程序指定界面方法
2019/05/07 Javascript
JS实现移动端在线签协议功能
2019/08/22 Javascript
JS forEach跳出循环2种实现方法
2020/06/24 Javascript
基于VSCode调试网页JavaScript代码过程详解
2020/07/20 Javascript
[20:21]《一刀刀一天》第十六期:TI国际邀请赛正式打响,总奖金超过550万
2014/05/23 DOTA
在Linux系统上部署Apache+Python+Django+MySQL环境
2015/12/24 Python
python访问抓取网页常用命令总结
2017/04/11 Python
Python基础知识_浅谈用户交互
2017/05/31 Python
Python利用matplotlib.pyplot绘图时如何设置坐标轴刻度
2018/04/09 Python
python使用PIL模块获取图片像素点的方法
2019/01/08 Python
python图像处理入门(一)
2019/04/04 Python
python实现车牌识别的示例代码
2019/08/05 Python
python验证码图片处理(二值化)
2019/11/01 Python
python爬虫搭配起Bilibili唧唧的流程分析
2020/12/01 Python
全球知名巧克力品牌:Godiva
2016/07/22 全球购物
摄影助理岗位职责
2014/02/07 职场文书
经典演讲稿开场白
2014/08/25 职场文书
教师工作失职检讨书
2014/09/18 职场文书
职工擅自离岗检讨书
2014/09/23 职场文书
财务人员廉洁自律心得体会
2016/01/13 职场文书
七年级作文之我的梦想
2019/10/16 职场文书
MySQL创建管理LIST分区
2022/04/13 MySQL