MongoDB - 十进制数值类型怎么处理?

43

我目前正在学习并应用 MongoDB 来完成一个小型的金融相关项目。


当我阅读 MongoDB 实战 时,其中提到:

BSON 数字类型常见的唯一问题就是缺少十进制支持。这意味着如果你计划在 MongoDB 中存储货币价值,你需要使用整数类型并将其值保留为分(即以“分”为单位进行存储)。


我的金融相关产品将涉及一些货币值,但我有点困惑或担心上述声明。以下是我的问题:

  1. 我能在我的项目中使用 double 存储这些货币值吗?
  2. 如果我直接使用 double 存储这些货币值,会发生什么或有什么后果?
  3. 如果十进制类型对于金融产品是必需品,那是否使用 MongoDB 是个坏主意?
  4. 什么意思是“你需要使用整数类型并将其保留为分”?这是否意味着如果我要存储 “1.34 美元”,那么我应该存储 “134 分”?

2
我问了一个类似的问题,WiredPrairie 指向了仍然开放的问题 https://jira.mongodb.org/browse/SERVER-1393 - PeterFromCologne
2
Mongodb现在有十进制类型 - Salvador Dali
7个回答

43
如果您需要精确的财务表示,那么双精度或浮点值是不合适的,因为小数部分容易出现舍入误差。某些十进制值不能用基于二进制的浮点数表示,必须进行近似处理。
如果您想了解更少技术性的介绍,请参见The trouble with rounding floating point numbers;如果您想深入了解,请阅读What Every Computer Scientist Should Know About Floating-Point Arithmetic
在MongoDB文档中,建议使用整数类型(以分为单位存储值),以避免潜在的舍入误差。这种方法被描述为"Using a Scale Factor",并且是MongoDB 3.2及更早版本的一种通用解决方法,用于modelling monetary data
MongoDB 3.4 包括一个新的Decimal BSON类型,它提供了精确的计算货币数据字段的能力。

请问你能给我一个关于在MongoDB中使用整数类型(以分为单位存储值)的例子吗? - Jackson Tale
2
@JacksonTale:Kyle的博客上有一个例子:MongoDB和电子商务。虽然这篇文章已经写了几年(提到了MongoDB 1.5开发版本,当前生产版本是2.0,开发版本为2.1),但是基本的架构概念仍然适用。自那以后,MongoDB已经有了许多改进,例如更好的复制、默认启用的日志记录和即将推出的聚合框架 - Stennie
2
支持电子商务或金融应用中的十进制非常重要。在最好的情况下使用整数是一个hack。不同的货币使用不同数量的小数位,因此不可能仅仅假设可以使用整数来表示最后2或4位数字作为小数部分。如果有疑问,只需搜索Knight Capital,这家公司因其整数表示中的错误而在45分钟内损失了4.4亿美元。 - Panagiotis Kanavos
“本地的十进制类型可以处理小数点的额外格式,但不应对您选择使用哪个数据库产生重大影响。” 真的吗? - MatteoSp

6
MongoDb在3.4版本中添加了十进制数据类型的支持。同时,该数据类型也可以从shell中使用。
3.4版本新增了十进制数据类型,并且支持decimal128格式。decimal128格式支持最多34位小数(即有效数字),指数范围为-6143到+6144。
与double数据类型不同的是,decimal数据类型存储精确值而非近似值。例如,十进制NumberDecimal("9.99")具有精确值9.99,而double 9.99则具有近似值9.9900000000000002131628...。

请注意,如果使用NodeJS,则不直接使用NumberDecimal(string),而是使用Decimal128.fromString(string)。有关参考资料、示例代码等,请参见下面的答案。 - amota

4
如果您正在使用Mongoose,则可以在模式定义中使用getter/setter函数,例如:

如果您正在使用Mongoose,则可以在模式定义中使用getter/setter函数,例如:

function getDecimalNumber(val) {    return (val/1000000); }
function setDecimalNumber(val) {    return (val*1000000); }

适用于模式对象,如:
balance: { type: Number, default: 0, get: getDecimalNumber, set: setDecimalNumber },

要乘以或除以的零的数量取决于您需要的精度。


return (val/1000000).toFixed(2); - Stephan Kristyn

4

当您不想将货币存储为分值时,可以使用以下对象来存储1.34美元的货币:

{
    major: 1,
    minor: 34,
    currency: "USD"
}

使用这样的对象进行任何数学计算都不容易,并且不遵守商业四舍五入规则。不过,你也不应该在数据库上执行任何业务逻辑,特别是在像MongoDB这样的“愚蠢”数据库上。

你应该做的是在应用程序中使用一个实现基本货币数学运算并遵守舍入规则的Money类来序列化/反序列化这些对象,并在尝试使用不同的货币单位进行操作时抛出异常($12.34 + 14.95€ = 错误 - 必须先提供汇率将一种货币转换为另一种货币)。


1
尤其是在“促销产品”等领域,你会遇到每件商品0.00005美元的金额。那么“以分为单位保存值”的做法就不适用了。这时你需要一些像MySQL中的DECIMAL(12,6)这样的数据类型,或者像Philipp那样将主要部分和次要部分拆分,并添加一个名为“diviser”的第三个项目,允许你指定“次要”部分中有零存在。另一种选择是{"amount":123305,"precision":4},表示一个值为12.3305。你只需将金额除以10^4即可。 - Niek Oost

4

3

不是什么大事,但Mongo技术上提到了按1000进行扩展。当然,由于它是一致的,您可以按任何想要的比例进行扩展。 - conrad10781

3

NodeJS 用户:

请注意,如果您使用 NodeJS,则不直接使用 NumberDecimal(string),而是使用 Decimal128.fromString(string)。

参考资料:

示例:

const Decimal128 = require('mongodb').Decimal128

usersCollection.findOneAndUpdate({_id: ObjectID(this.data.userId), "myWells.wellId": ObjectID(this.data.wellId)},
  { 
    $set: { "myWells.$.interest": Decimal128.fromString(this.data.interest) }
  }
)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接