自定义Django的金额类型

近需要和财务数据打交道,就得为数据库里存储数据选择一个好的储存方式。一开始是想使用 DecimalField 的,毕竟这是Django的原生字段。可是使用下来发现了两点问题,一是 DecimalField 在数据库中储存使用的是numeric的类型(我使用的是PostgreSQL),如果没记错的话,numeric在数据库中应该是使用的字符串来储存的,那么在使用求和之类的聚合时可能效率上会有问题;二是 DecimalField 返回的 Decimal 类型并不被CJSON或者SimpleJSON支持,需要手动转换,比较麻烦。

由于这两个问题,便萌生了自定义字段类型的想法。基本的思路是使用整型来储存,由于财务数据也就两位小数,把每个数据乘以100就能不失精度的储存。然后在获取的时候,在转换回来就可以了。看上去也不是什么复杂的工作,便立刻开始着手做了。

我的向来的习惯时,如果是自定义Django类型的话,首先搬出Django源码看看。看了一下 PositiveIntegerField 的实现,觉得也就是在 to_python()get_db_prep_value() 上下点文章罢了,于是就出了下面一段代码:

class MoneyField(models.IntegerField):
    def get_db_prep_value(self, value):
        if value is None:
            return None
        return int(value * 100)

    def to_python(self, value):
        if value is None or isinstance(value, float):
            return value
        try:
            return float(value) / 100
        except (TypeError, ValueError):
            raise exceptions.ValidationError(
                "This value must be an integer or a string represents an integer.")

在shell里面试了一下,查询和储存都没有问题,但是在获取的时候,返回值并没有像我想像的那样转换回浮点。于是继续看源码,可是看了大半天也没弄出什么名堂出来。暂时没有什么办法的情况下,只好直接使用 IntegerField ,然后给模型增加一个getter和setter property将就了。

今天无意中翻Django的文档发现,如果希望 to_python 起效,必须把这个字段类型的 __metaclass 设置为 SubfieldBase 的值才行。尝试以后果然起效,最后再为 ModelForm 把表单类型重新定义一下,基本完工了。

class MoneyField(models.IntegerField):
    '''
    It is supposed the aggregation on integer is fast than numeric type in
    database duo to how they are stored. As money only have 2 decimal place,
    it can be converted to an Integer despite of its decimal point.

    The python class decimal.Decimal also has a shortcoming that it is not
    json serializable. This money field appears as a float number to the
    front end, so it does not meet the headache as DecimalField.
    '''
    __metaclass__ =  models.SubfieldBase # very important for to_python

    def get_db_prep_value(self, value):
        if value is None:
            return None
        from decimal import Decimal
        return int(Decimal(str(value)) * 100)

    def to_python(self, value):
        if value is None or isinstance(value, float):
            return value
        from decimal import Decimal
        try:
            return float(Decimal(value) / 100)
        except (TypeError, ValueError):
            raise exceptions.ValidationError(
                "This value must be an integer or a string represents an integer.")

    def formfield(self, **kwargs):
        from django.forms import FloatField
        defaults = {'form_class': FloatField}
        defaults.update(kwargs)
        return super(MoneyField, self).formfield(**defaults)

不过现在的代码还有一个问题,当使用了 values()values_list()only()defer() 这样的部分选取数值的查询方法时, to_python() 仍然不会被调用。可是我试过了,拿 django.db.models.DateField 为例,这样查询时这样字段的值还是会转换为 datetime.date 类型的。不过暂时我也找不到其他的办法了,先将就用着。出现这样的调用时手动转换一下吧,直到俺找到解决办法…… 各位路过打酱油的童鞋如果知道的话也麻烦留言告诉我一下:)

对“自定义Django的金额类型”的1条评论

  1. Avatar

    嗯,最近也想用django弄个股票类的网站,也遇到用了values_list后弄出的日期数据是datetime.date类型的,不知道该怎样解决,博主是用什么方法解决的呢?

发表评论

评论备注:

  1. 留言时的头像是Gravatar提供的服务。
  2. By submitting a comment here you grant this site a perpetual license to reproduce your words and name/web site in attribution. So, you don't fully own your words, so to speak.