我正在尝试在Django中编写一个自定义的PostgreSQL函数,它将日期时间强制转换为查询集内的指定时区。我在db函数的第一次传递看起来像这样:
from django.db.models.expressions import Func
class DateTimeInTimezone(Func):
template="%(expressions)s AT TIME ZONE %(tz_info)s"
这个函数适用于我将时区字符串直接传递给函数的简单情况:
MyModel.objects.filter(timefield__gte=DateTimeInTimezone(Now(), tz_info='EST'))
然而,它不适用于更复杂的情况,即在模型的某些字段上定义时区。考虑以下设计的例子:
class User(models.Model):
time_zone = models.CharField()
class Meeting(models.Model):
users = models.ManyToManyField(User, related_name='meetings')
start_time = models.DateTimeField() # in UTC
end_time = models.DateTimeField() # in UTC
要回答“用户将在当地时间今天中午12点参加会议的内容?”这一问题,我需要对此查询集进行一些修改:
noon_utc = ...
User.objects.filter(
meetings__start_time__lte=DateTimeInTimezone(noon_utc, tz_info=F('time_zone')),
meetings__end_time__gt=DateTimeInTimezone(noon_utc, tz_info=F('time_zone'))
)
但是,正如当前所写,DateTimeInTimezone
只会将字符串F('time_zone')
注入sql而不是解析字段。
是否可以为此功能添加对F Expressions的支持?还有其他方法我应该考虑吗?
答案 0 :(得分:3)
一个简单的解决方案是 参数 arg_joiner
:
class DateTimeInTimezone(Func):
function = ''
arg_joiner = ' AT TIME ZONE '
def __init__(self, timestamp, tz_info):
super(DateTimeInTimezone, self).__init__(timestamp, tz_info)
方法__init__
仅用于具有明确参数名称的可读性。如果参数由arity
声明,则__init__
并不重要。
如果可读性不重要, oneliner 函数对于快速开发非常有用:
...filter(
meetings__start_time__lte=Func(noon_utc, tz_info=F('time_zone'), arg_joiner=' AT TIME ZONE ', function=''),
)
<强>验证强>
>>> qs = User.objects.filter(...)
>>> print(str(qs.query))
SELECT ... WHERE ("app_meeting"."start_time" <= ((2017-10-03 08:18:12.663640 AT TIME ZONE "app_user"."time_zone")) AND ...)
答案 1 :(得分:1)
找到一个可接受的解决方案。我像这样覆盖as_sql
函数的方法,允许django内部解析F表达式,然后将其分离回一个我可以在模板的不同部分使用的kwarg。
class DateTimeInTimezone(Func):
'''
Coerce a datetime into a specified timezone
'''
template="%(expressions)s AT TIME ZONE %(tz_info)s"
arity = 2
def as_sql(self, compiler, connection, function=None, template=None, arg_joiner=None, **extra_context):
connection.ops.check_expression_support(self)
sql_parts = []
params = []
for arg in self.source_expressions:
arg_sql, arg_params = compiler.compile(arg)
sql_parts.append(arg_sql)
params.extend(arg_params)
data = self.extra.copy()
data.update(**extra_context)
# Use the first supplied value in this order: the parameter to this
# method, a value supplied in __init__()'s **extra (the value in
# `data`), or the value defined on the class.
if function is not None:
data['function'] = function
else:
data.setdefault('function', self.function)
template = template or data.get('template', self.template)
arg_joiner = arg_joiner or data.get('arg_joiner', self.arg_joiner)
data['expressions'] = data['field'] = arg_joiner.join(sql_parts)
parts = data['expressions'].split(', ')
data['expressions'] = parts[0]
data['tz_info'] = parts[1]
return template % data, params
我在data['expressions']
和最终return template % data, params
的分配之间添加了三行。这不是一个很好的长期解决方案,因为这种方法的django内部结构可能会在下一个版本中发生变化,但它暂时适合我的需求。