django与数据库交互关于当前时间的坑
背景
在线上服务中使用时间进行数据库操作时发现异常,而在本地环境无法成功复现此问题,导致难以进行故障排查。
核心问题
view.py
class XxxViewSet(viewsets.ModelViewSet):
queryset = Xxx.objects.with_status().order_by("status", "-start_time")
managers.py
def with_status(self):
"""添加status排序字段"""
cur_time = datetime.now(pytz.utc)
queryset = self.annotate(
status=Case(
When(end_time__lt=cur_time, then=Value(2)),
When(start_time__gt=cur_time, then=Value(1)),
default=Value(0),
output_field=IntegerField(),
)
)
return queryset
问题的关键点
由于viewsets.ModelViewSet
中使用了queryset
属性,程序不会在每次请求时都重新计算当前时间,而是使用缓存的查询集。这意味着时间过滤条件并不会随着时间实时更新,而是固定在视图集类被加载时的时间。
分析本地无法复现原因
本地开发经常使用热部署,即服务频繁重启,这导致定义的cur_time
变量不断被重置。而线上环境服务不会如此频繁重启,因而很难注意到这个问题。建议在本地创建一条时间精确到秒的记录,以此来模拟线上环境并复现问题。
问题的根源
在Django中与数据库交互时,应使用Django数据库函数中的当前时间而非Python标准库中的时间:
使用Django的Now
函数:
from django.db.models.functions import Now
而不是使用python 的时间
from datetime import datetime
否则会把时间设置为定值
从sql的角度理解就是
最终方案
修改使用数据库时间
managers.py
from django.db.models.functions import Now
def with_status(self):
"""添加status排序字段"""
cur_time = Now()
queryset = self.annotate(
status=Case(
When(end_time__lt=cur_time, then=Value(2)),
When(start_time__gt=cur_time, then=Value(1)),
default=Value(0),
output_field=IntegerField(),
)
)
return queryset
结论与建议
使用Django django.db.models.functions.Now()
函数替代Python datetime.datetime.now()
在Django应用程序的时间处理中具有几个优点与潜在的缺点:
优点:
1.在Django与数据库交互时,如果需要获取当前时间,应优先考虑使用django.db.models.functions.Now()
,而不是datetime.datetime.now()
,特别是当涉及到使用类属性queryset
的情况。这样可以确保每次请求都能反映真实的当前时间,避免由于查询集缓存所导致的时间判断错误。确保时间数据的一致性与准确性。
2. 数据库兼容性:Now()
函数自动适应不同数据库系统的时间函数,降低了数据库之间兼容性问题的风险。
3. 性能:如果数据库后端支持,使用Now()
可能会比从应用层传递时间戳到数据库更优化,因为时间运算直接在数据库层完成。
4. 时区一致性:Now()
函数遵守Django的时区设置,自动处理时区转换,减少了手动处理时区问题的复杂度。
缺点:
- 依赖数据库时钟:使用
Now()
函数意味着依赖数据库服务器的时钟。如果数据库服务器的时间配置不正确,可能会导致问题。 - 数据库执行时间:由于
Now()
函数在数据库执行查询时生成时间,如果一个请求涉及多个查询,而查询之间有延迟,这可能会导致时间上的微小不一致。 - 测试复杂性:使用
Now()
可能会使单元测试更复杂,因为在测试环境中控制或模拟数据库返回的Now()
值通常比使用固定的datetime
值更困难。
总结
- 在Django与数据库交互时,如果需要获取当前时间,应优先考虑使用
django.db.models.functions.Now()
,而不是datetime.datetime.now()
,特别是当涉及到使用类属性queryset
的情况。这样可以确保每次请求都能反映真实的当前时间,避免由于查询集缓存所导致的时间判断错误。确保时间数据的一致性与准确性。 - 若使用queryset应该避免存在动态计算的情况,比如上述例子的status字段计算,
queryset = Announcement.objects.all()
程序不会在每次请求时都重新计算当前时间,而是使用缓存的查询集。