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可能会产生死锁,具体可以看我的上一篇文章。