2

我正在尝试在 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 表达式的支持?我应该考虑其他一些方法吗?

4

2 回答 2

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 ...)
于 2017-10-03T09:24:54.220 回答
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']我在 assignment of和 final之间添加了三行return template % data, params。这不是一个很好的长期解决方案,因为这种方法的 django 内部结构可能会在下一个版本中发生变化,但它暂时适合我的需要。

于 2017-04-18T19:53:15.717 回答