Django中涉及金融的项目

在Django中,如果一个项目涉及了金融,他的要求是十分严格的。

所以嘞,这里就有一些坑,很多坑,第一次开发的时候很容易出现一系列的错误

在涉及金融计算的地方,不能使用float类型

什么鬼,但事实就是如此,千万不要用float进行计算.....

所以,Python中为我们提供了一个专门的模块计算,decimal

 而同样的,Django中也提供了相应的计算字段,DecimalField

class DecimalField(max_digits=None, decimal_places=None[, **options])
  • max_digits:表示最大位数
  • decimal_places:表示小数点后面的位数
  • 假设你最多存999人民币,小数点要精确到2位,需要的max_digits就为6,decimal_places就为2

下面是一些例子

 1.在models.py中定义:

from django.db import models

class UserProfile(models.Model):
    price = models.DecimalField(max_digits=16,decimal_places=2)

 

2.添加一条记录:

from decimal import Decimal

obj = models.UserProfile.objects.create(price = Decimal("123.45"))

 

3.更新记录:

obj.price -= Decimal("1.00")

obj.save()

 

4.我们输出查看一下SQL语句:

from django.db import connection

print(connection.queries[-1])

//UPDATE `table` SET `price` = '122.45' WHERE `id` = 1

 

我们之前使用的update会有问题。在并发很高的时候,会遇到类似多线程的问题,因为加减操作都在客户端,某个线程写入price的时候,可能之前拿到的已经被别人更新过了,所以我们需要原子写入。

UPDATE `table` SET `price` = 'price' - '1.00' WHERE `id` = 1

 

在ORM及是使用F()

obj.price = F("price") - Decimal("1.00")

obj.save(update_fields = ["price"])

 

注意:在调用save()方法的时候,我们可以用update_fields传入需要update的字段。否则Django可能会把所有的字段都放在SQL中,影响效率


 

好吧,上面看似已经是把问题都解决了,但是只是看似。

我们在数据库中,定义的字段的精确到小数点后两位,如果我减去一个3位的小数会怎样呢?

obj.price = F("price") - Decimal("1.001")

obj.save(update_field=["price"])

 

好吧,结果不出意料的报错了

会抛出一个Traceback的错误

这个错误其实是MySQL抛出的一个异常,所以传递到了Django,使更新操作无法成功

怎么解决呢?

//我们手动写个方法进行一个精度的转换

def to_decimal(s,precision=2):
    
    r = pow(10,precision+1)
    v = s if type(s) is Decimal else Decimal(str(s))

    try:
        return Decimal(round(int(v * r),-1))/r
    except:
        return Decimal(s)

obj.price = F("price") - to_decimal("1.001",2)
obj.save(update_fields=["price"])

 

 这样貌似有解决了一个问题。。。实际呢?

这只是加或者减,如果是乘除呢?

由于这是MySQL层次上出的错误,也就是说实在最后存储的上面出的错,我们是没有办法在Django的层面上对计算出来的结果在进行一次类型转换的。这时候怎么办,只有上raw sql了。

//完整一点
//这次带上事务

from django.db import transaction,connection

try:
    with transaction.atomic():
        cursor = connection.cursor()
        ret = cursor.execute(
            "UPDATE 'table' SET 'price'=CAST(('price'*%s) AS DECIMAL(16,2)) WHERE 'id' = %s ",
            [Decimal("1.001"),obj.id]
    )
except:
    print("失败")

 

 注意:在MySQL层面上,我们使用CAST(%s AS DECIMAL(16,2))来把结果转化为price字段同样个是Decimal类型

返回更新数据的行数,如果成功了,ret就是1


 

你以为结束了?

 太天真了

注意:如果是事务操作,一定要考虑到多线程并发的造成的数据冲突的问题

即:假设连个线程获取了同意对象,进行了更改,怎么办?

这里就要用到select_for_update()

注意:这点很重要,金钱的操作,我们最好不要使用自增自减运算,而是使用select_for_update()的行级索来避免冲突。

所以嘞,我们的例子又可以改进了。

try:
    with transaction.atomic():
        locked_obj =UserProfile.objects.select_for_update().get(pk=obj.id)
locked_obj.price
-= to_decimal('11.11111', 2)
assert locked_obj.price >= 0 #断言判断是不是合理
locked_obj.save(update_fields
=['price'])
except:
print 'save failed'

 到这里,才算是告一段落。

还有,补充一点:

  网上都说使用select_for_update可能会产生死锁,具体可以看我的上一篇文章。

  Django中管理并发操作

 

posted @ 2018-07-26 16:22  "%201  阅读(382)  评论(0编辑  收藏  举报