模型货币数据

在本页面

Overview

处理货币数据的应用程序通常需要能够捕获货币的分数单位,并且在执行算术运算时需要精确精确地模拟十进制舍入。许多现代系统(即 float,double)使用的基于二进制的浮点算法无法表示精确的十进制小数,并且需要某种程度的近似使其不适用于货币算法。在对货币数据进行建模时,此约束是重要的考虑因素。

在 MongoDB 中,有几种使用数字模型和非数字模型对货币数据进行建模的方法。

Numeric Model

如果您需要查询数据库以获取精确的,math 上有效的匹配项或需要执行服务器端算术(例如$inc$mul聚合框架算法),则数字模型可能是合适的。

以下方法遵循数字模型:

  • 使用十进制 BSON 类型是基于十进制的浮点格式,能够提供精确的精度。在 MongoDB 3.4 版和更高版本中可用。

  • 使用比例因子通过乘以 10 的比例因子将货币值转换为 64 位整数(long BSON 类型)。

Non-Numeric Model

如果不需要对货币数据执行服务器端算术,或者如果服务器端近似值足够,那么使用非数字模型对货币数据进行建模可能是合适的。

以下方法遵循非数字模型:

  • 使用两个字段作为货币值:一个字段将确切的货币值存储为非数字string,另一个字段存储值的基于二进制的浮点(double BSON 类型)近似值。

Note

本页中提到的算术是指由mongodmongos执行的服务器端算术,而不是 Client 端算术。

Numeric Model

使用十进制 BSON 类型

3.4 版的新功能。

decimal BSON type使用基于 IEEE 754 十进制 128 的基于十进制的浮点编号格式。与基于二进制的浮点格式(即double BSON 类型)不同,decimal128 不能近似十进制值,并且能够提供处理货币数据所需的确切精度。

使用NumberDecimal()构造函数从mongo shell decimal分配和查询值。下面的示例将包含汽油价格的文档添加到gasprices集合中:

db.gasprices.insert{ "_id" : 1, "date" : ISODate(), "price" : NumberDecimal("2.099"), "station" : "Quikstop", "grade" : "regular" }

以下查询与上面的文档匹配:

db.gasprices.find( { price: NumberDecimal("2.099") } )

有关decimal类型的更多信息,请参见NumberDecimal

将值转换为十进制

可以通过执行一次转换或通过修改应用程序逻辑以在访问记录时执行转换来将集合的值转换为decimal类型。

一次性收藏转换

通过遍历集合中的所有文档,将货币值转换为decimal类型,然后将文档写回到集合中,可以转换集合。

Note

强烈建议将decimal值添加到文档中作为新字段,并在新字段的值经过验证后再删除旧字段。

Warning

确保在隔离的测试环境中测试decimal转换。使用 MongoDB 3.4 版创建或修改数据文件后,它们将不再与以前的版本兼容,并且不支持降级包含小数的数据文件。

比例因子转换:

请考虑以下使用Scale Factor方法并将货币值保存为代表美分数的 64 位整数的集合:

{ "_id" : 1, "description" : "T-Shirt", "size" : "M", "price" : NumberLong("1999") },
{ "_id" : 2, "description" : "Jeans", "size" : "36", "price" : NumberLong("3999") },
{ "_id" : 3, "description" : "Shorts", "size" : "32", "price" : NumberLong("2999") },
{ "_id" : 4, "description" : "Cool T-Shirt", "size" : "L", "price" : NumberLong("2495") },
{ "_id" : 5, "description" : "Designer Jeans", "size" : "30", "price" : NumberLong("8000") }

通过使用$multiply运算符将priceNumberDecimal("0.01")相乘,可以将long值转换为适当格式的decimal值。以下聚合管道将转换后的值分配给$addFields阶段中的新priceDec字段:

db.clothes.aggregate(
  [
    { $match: { price: { $type: "long" }, priceDec: { $exists: 0 } } },
    {
      $addFields: {
        priceDec: {
          $multiply: [ "$price", NumberDecimal( "0.01" ) ]
        }
      }
    }
  ]
).forEach( ( function( doc ) {
  db.clothes.save( doc );
} ) )

可以使用db.clothes.find()查询来验证聚合管道的结果:

{ "_id" : 1, "description" : "T-Shirt", "size" : "M", "price" : NumberLong(1999), "priceDec" : NumberDecimal("19.99") }
{ "_id" : 2, "description" : "Jeans", "size" : "36", "price" : NumberLong(3999), "priceDec" : NumberDecimal("39.99") }
{ "_id" : 3, "description" : "Shorts", "size" : "32", "price" : NumberLong(2999), "priceDec" : NumberDecimal("29.99") }
{ "_id" : 4, "description" : "Cool T-Shirt", "size" : "L", "price" : NumberLong(2495), "priceDec" : NumberDecimal("24.95") }
{ "_id" : 5, "description" : "Designer Jeans", "size" : "30", "price" : NumberLong(8000), "priceDec" : NumberDecimal("80.00") }

如果您不想使用decimal值添加新字段,则可以覆盖原始字段。以下update()方法首先检查price是否存在并且它是long,然后将long的值转换为decimal并将其存储在price字段中:

db.clothes.update(
  { price: { $type: "long" } },
  { $mul: { price: NumberDecimal( "0.01" ) } },
  { multi: 1 }
)

可以使用db.clothes.find()查询验证结果:

{ "_id" : 1, "description" : "T-Shirt", "size" : "M", "price" : NumberDecimal("19.99") }
{ "_id" : 2, "description" : "Jeans", "size" : "36", "price" : NumberDecimal("39.99") }
{ "_id" : 3, "description" : "Shorts", "size" : "32", "price" : NumberDecimal("29.99") }
{ "_id" : 4, "description" : "Cool T-Shirt", "size" : "L", "price" : NumberDecimal("24.95") }
{ "_id" : 5, "description" : "Designer Jeans", "size" : "30", "price" : NumberDecimal("80.00") }

Non-Numeric Transformation:

考虑以下使用non-numeric模型并将货币值保存为string并精确表示该值的集合:

{ "_id" : 1, "description" : "T-Shirt", "size" : "M", "price" : "19.99" }
{ "_id" : 2, "description" : "Jeans", "size" : "36", "price" : "39.99" }
{ "_id" : 3, "description" : "Shorts", "size" : "32", "price" : "29.99" }
{ "_id" : 4, "description" : "Cool T-Shirt", "size" : "L", "price" : "24.95" }
{ "_id" : 5, "description" : "Designer Jeans", "size" : "30", "price" : "80.00" }

以下函数首先检查price是否存在并且它是string,然后将string值转换为decimal值并将其存储在priceDec字段中:

db.clothes.find( { $and : [ { price: { $exists: true } }, { price: { $type: "string" } } ] } ).forEach( function( doc ) {
  doc.priceDec = NumberDecimal( doc.price );
  db.clothes.save( doc );
} );

该函数不向命令行输出任何内容。可以使用db.clothes.find()查询验证结果:

{ "_id" : 1, "description" : "T-Shirt", "size" : "M", "price" : "19.99", "priceDec" : NumberDecimal("19.99") }
{ "_id" : 2, "description" : "Jeans", "size" : "36", "price" : "39.99", "priceDec" : NumberDecimal("39.99") }
{ "_id" : 3, "description" : "Shorts", "size" : "32", "price" : "29.99", "priceDec" : NumberDecimal("29.99") }
{ "_id" : 4, "description" : "Cool T-Shirt", "size" : "L", "price" : "24.95", "priceDec" : NumberDecimal("24.95") }
{ "_id" : 5, "description" : "Designer Jeans", "size" : "30", "price" : "80.00", "priceDec" : NumberDecimal("80.00") }
应用逻辑转换

可以从应用程序逻辑中执行到decimal类型的转换。在这种情况下,应用程序修改为在访问记录时执行转换。

典型的应用程序逻辑如下:

  • 测试新字段是否存在并且为decimal类型

  • 如果新的decimal字段不存在:

  • 通过正确转换旧字段值来创建它

    • 删除旧字段

    • 保持转换后的记录

使用比例因子

Note

如果您使用的是 MongoDB 3.4 版或更高版本,则使用decimal类型建模货币数据比使用Scale Factor方法更可取。

使用比例因子方法对货币数据进行建模:

  • 确定货币值所需的最大精度。例如,对于以USD货币表示的货币值,您的应用程序可能要求精度低至十分之一美分。

  • 通过将货币值乘以 10 的幂将货币值转换为整数,以确保所需的最大精度成为整数的最低有效位。例如,如果所需的最大精度是百分之一的十分之一,则将货币值乘以 1000.

  • 存储转换后的货币值。

例如,以下代码将9.99 USD缩放 1000,以保持精度高达十分之一美分。

{ price: 9990, currency: "USD" }

该模型假定对于给定的货币值:

  • 比例因子对于货币是一致的;即给定货币的缩放比例相同。

  • 比例因子是货币的恒定且已知的属性;即应用程序可以根据货币确定比例因子。

使用此模型时,应用程序必须在执行值的适当缩放时保持一致。

有关此模型的用例,请参见Numeric Model

Non-Numeric Model

要使用非数字模型对货币数据进行建模,请将值存储在两个字段中:

  • 在一个字段中,将确切的货币值编码为非数字数据类型;例如BinDatastring

  • 在第二个字段中,存储精确值的双精度浮点近似值。

以下示例使用非数字模型存储9.99 USD的价格和0.25 USD的费用:

{
  price: { display: "9.99", approx: 9.9900000000000002, currency: "USD" },
  fee: { display: "0.25", approx: 0.2499999999999999, currency: "USD" }
}

一定要小心,应用程序可以使用数字近似值在字段上执行范围和排序查询。但是,将近似字段用于查询和排序操作要求应用程序执行 Client 端后处理,以解码精确值的非数字表示形式,然后根据精确货币值过滤掉返回的文档。

有关此模型的用例,请参见Non-Numeric Model