2

我想在Django中編寫一個自定義PostgreSQL函數,該函數會將日期時間強制轉換爲查詢集內的指定時區。我在DB函數第一遍看起來是這樣的:DjangoORM:在自定義數據庫函數中解析F表達式

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的支持添加到此函數中?我應該考慮一些其他方法嗎?

回答

1

一個簡單的解決方案是可能的參數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__僅用於易讀目的用的參數明確名稱。如果參數是由__init__聲明的,那麼arity並不重要。

如果可讀性並不重要一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 ...) 
+0

嘿,這太棒了!感謝您提供此解決方案。我將你的回答作爲接受的答案,因爲它肯定更適合猴子補丁(正如你在評論中提到的那樣)。 – chukkwagon

1

找到了一個可接受的解決方案。我覆蓋as_sql方法的功能就像這樣,允許Django內部解析F表達式,然後將它分離出來,然後將其分離出來,然後我可以在模板的不同部分使用它。

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內部版本可能在下一個版本中發生變化,但它適合我目前的需求。

+0

+1它的工作原理,但猴子修補是不得已而爲之,因爲Django升級後維護可能會很費力。 – hynekcer